Reviewed by Tim Hatcher's rubberstamp
[WebKit-https.git] / WebKit / Misc / WebFileDatabase.m
1 /*
2  * Copyright (C) 2005 Apple Computer, Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 #import <WebKit/WebFileDatabase.h>
30 #import <WebKit/WebKitLogging.h>
31 #import <WebKit/WebLRUFileList.h>
32 #import <WebKit/WebNSFileManagerExtras.h>
33
34 #import <fcntl.h>
35 #import <fts.h>
36 #import <pthread.h>
37 #import <string.h>
38 #import <sys/stat.h>
39 #import <sys/types.h>
40 #import <sys/mman.h>
41
42 #if ERROR_DISABLED
43 #define BEGIN_EXCEPTION_HANDLER
44 #define END_EXCEPTION_HANDLER
45 #else
46 #define BEGIN_EXCEPTION_HANDLER NS_DURING
47 #define END_EXCEPTION_HANDLER \
48     NS_HANDLER \
49         LOG_ERROR("Uncaught exception: %@ [%@] [%@]", [localException class], [localException reason], [localException userInfo]); \
50     NS_ENDHANDLER
51 #endif
52
53 static pthread_once_t databaseInitControl = PTHREAD_ONCE_INIT;
54 static NSNumber *WebFileDirectoryPOSIXPermissions;
55 static NSNumber *WebFilePOSIXPermissions;
56 static NSRunLoop *syncRunLoop;
57
58 #define UniqueFilePathSize (34)
59 static void UniqueFilePathForKey(id key, char *buffer);
60
61 #define MinThreadPriority (10)
62 static int SetThreadPriority(int priority);
63
64 typedef enum
65 {
66     WebFileDatabaseSetObjectOp,
67     WebFileDatabaseRemoveObjectOp,
68 } WebFileDatabaseOpcode;
69
70 enum
71 {
72     MAX_UNSIGNED_LENGTH = 20, // long enough to hold the string representation of a 64-bit unsigned number
73     SYNC_IDLE_THRESHOLD = 10,
74 };
75
76 // interface WebFileDatabaseOp -------------------------------------------------------------
77
78 @interface WebFileDatabaseOp : NSObject
79 {
80     WebFileDatabaseOpcode opcode;
81     id key;
82     id object; 
83 }
84
85 +(id)opWithCode:(WebFileDatabaseOpcode)opcode key:(id)key object:(id)object;
86 -(id)initWithCode:(WebFileDatabaseOpcode)opcode key:(id)key object:(id)object;
87
88 -(WebFileDatabaseOpcode)opcode;
89 -(id)key;
90 -(id)object;
91 -(void)perform:(WebFileDatabase *)target;
92
93 @end
94
95
96 // implementation WebFileDatabaseOp -------------------------------------------------------------
97
98 @implementation WebFileDatabaseOp
99
100 +(id)opWithCode:(WebFileDatabaseOpcode)theOpcode key:(id)theKey object:(id)theObject
101 {
102     return [[[WebFileDatabaseOp alloc] initWithCode:theOpcode key:theKey object:theObject] autorelease];
103 }
104
105 -(id)initWithCode:(WebFileDatabaseOpcode)theOpcode key:(id)theKey object:(id)theObject
106 {
107     ASSERT(theKey);
108
109     if ((self = [super init])) {
110         
111         opcode = theOpcode;
112         key = [theKey retain];
113         object = [theObject retain];
114         
115         return self;
116     }
117   
118     return nil;
119 }
120
121 -(WebFileDatabaseOpcode)opcode
122 {
123     return opcode;
124 }
125
126 -(id)key
127 {
128     return key;
129 }
130
131 -(id)object
132 {
133     return object;
134 }
135
136 -(void)perform:(WebFileDatabase *)target
137 {
138     ASSERT(target);
139
140     switch (opcode) {
141         case WebFileDatabaseSetObjectOp:
142             [target performSetObject:object forKey:key];
143             break;
144         case WebFileDatabaseRemoveObjectOp:
145             [target performRemoveObjectForKey:key];
146             break;
147         default:
148             ASSERT_NOT_REACHED();
149             break;
150     }
151 }
152
153 -(void)dealloc
154 {
155     [key release];
156     [object release];
157     [super dealloc];
158 }
159
160 @end
161
162
163 // interface WebFileDatabasePrivate -----------------------------------------------------------
164
165 @interface WebFileDatabase (WebFileDatabasePrivate)
166
167 -(void)_createLRUList:(id)arg;
168 -(void)_truncateToSizeLimit:(unsigned)size;
169
170 @end
171
172 // implementation WebFileDatabasePrivate ------------------------------------------------------
173
174 @implementation WebFileDatabase (WebFileDatabasePrivate)
175
176 static int SetThreadPriority(int priority) 
177 {
178     struct sched_param sp;
179
180     memset(&sp, 0, sizeof(struct sched_param));
181     sp.sched_priority=priority;
182     if (pthread_setschedparam(pthread_self(), SCHED_OTHER, &sp) == -1) {
183         LOG_ERROR("Failed to change priority.");
184         return -1;
185     }
186     return 0;
187 }
188
189 static void UniqueFilePathForKey(id key, char *buffer)
190 {
191     const char *s;
192     UInt32 hash1;
193     UInt32 hash2;
194     CFIndex len;
195     CFIndex cnt;
196     
197     s = [[[[key description] lowercaseString] stringByStandardizingPath] UTF8String];
198     len = strlen(s);
199
200     // compute first hash    
201     hash1 = len;
202     for (cnt = 0; cnt < len; cnt++) {
203         hash1 += (hash1 << 8) + s[cnt];
204     }
205     hash1 += (hash1 << (len & 31));
206
207     // compute second hash    
208     hash2 = len;
209     for (cnt = 0; cnt < len; cnt++) {
210         hash2 = (37 * hash2) ^ s[cnt];
211     }
212
213 #ifdef __LP64__
214     snprintf(buffer, UniqueFilePathSize, "%.2u/%.2u/%.10u-%.10u.cache", ((hash1 & 0xff) >> 4), ((hash2 & 0xff) >> 4), hash1, hash2);
215 #else
216     snprintf(buffer, UniqueFilePathSize, "%.2lu/%.2lu/%.10lu-%.10lu.cache", ((hash1 & 0xff) >> 4), ((hash2 & 0xff) >> 4), hash1, hash2);
217 #endif
218 }
219
220 -(void)_createLRUList:(id)arg
221 {
222     SetThreadPriority(MinThreadPriority + 1); // make this a little higher priority than the syncRunLoop thread
223
224     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
225
226     BEGIN_EXCEPTION_HANDLER
227     
228     WebLRUFileList *fileList = WebLRUFileListCreate();
229     WebLRUFileListRebuildFileDataUsingRootDirectory(fileList, [path fileSystemRepresentation]);
230     lru = fileList;
231
232     END_EXCEPTION_HANDLER
233
234     LOG(FileDatabaseActivity, "lru list created");
235
236     [pool drain];
237 }
238
239 -(void)_truncateToSizeLimit:(unsigned)size
240 {
241     NSFileManager *defaultManager;
242     
243     if (!lru || size > [self usage]) {
244         return;
245     }
246
247     if (size == 0) {
248         [self removeAllObjects];
249     }
250     else {
251         defaultManager = [NSFileManager defaultManager];
252         [mutex lock];
253         while ([self usage] > size) {
254             char uniqueKey[UniqueFilePathSize];
255             if (!WebLRUFileListGetPathOfOldestFile(lru, uniqueKey, UniqueFilePathSize)) {
256                 break;
257             }
258             NSString *filePath = [[NSString alloc] initWithFormat:@"%@/%s", path, uniqueKey];
259             [defaultManager _webkit_removeFileOnlyAtPath:filePath];
260             [filePath release];
261             WebLRUFileListRemoveOldestFileFromList(lru);
262         }
263         [mutex unlock];
264     }
265 }
266
267 @end
268
269
270 // implementation WebFileDatabase -------------------------------------------------------------
271
272 @implementation WebFileDatabase
273
274 // creation functions ---------------------------------------------------------------------------
275 #pragma mark creation functions
276
277 +(void)_syncLoop:(id)arg
278 {
279     SetThreadPriority(MinThreadPriority);
280
281     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
282     NSPort *placeholderPort;
283
284     BEGIN_EXCEPTION_HANDLER
285
286     syncRunLoop = [NSRunLoop currentRunLoop];
287
288     while (YES) {
289         BEGIN_EXCEPTION_HANDLER
290         // we specifically use an NSRunLoop here to get autorelease pool support
291         placeholderPort = [NSPort port];
292         [syncRunLoop addPort:placeholderPort forMode:NSDefaultRunLoopMode];
293         [syncRunLoop run];
294         [syncRunLoop removePort:placeholderPort forMode:NSDefaultRunLoopMode];
295         END_EXCEPTION_HANDLER
296     }
297
298     END_EXCEPTION_HANDLER
299
300     [pool drain];
301 }
302
303 static void databaseInit()
304 {
305     // set file perms to owner read/write/execute only
306     WebFileDirectoryPOSIXPermissions = [[NSNumber numberWithInt:(WEB_UREAD | WEB_UWRITE | WEB_UEXEC)] retain];
307
308     // set file perms to owner read/write only
309     WebFilePOSIXPermissions = [[NSNumber numberWithInt:(WEB_UREAD | WEB_UWRITE)] retain];
310
311     [NSThread detachNewThreadSelector:@selector(_syncLoop:) toTarget:[WebFileDatabase class] withObject:nil];
312 }
313
314 -(id)initWithPath:(NSString *)thePath
315 {
316     pthread_once(&databaseInitControl, databaseInit);
317
318     if (!(self = [super init])) 
319         return nil;
320         
321     path = [[thePath stringByStandardizingPath] copy];
322     if (thePath == nil) {
323         [self release];
324         return nil;
325     }
326     
327     isOpen = NO;
328     sizeLimit = 0;
329     usage = 0;
330
331     ops = [[NSMutableArray alloc] init];
332     setCache = [[NSMutableDictionary alloc] init];
333     removeCache = [[NSMutableSet alloc] init];
334     timer = nil;
335     mutex = [[NSRecursiveLock alloc] init];
336     
337     return self;
338 }
339
340 -(void)dealloc
341 {
342     [path release];
343     [super dealloc];
344 }
345
346
347 -(void)setTimer
348 {
349     if (timer == nil) {
350         NSDate *fireDate = [[NSDate alloc] initWithTimeIntervalSinceNow:SYNC_IDLE_THRESHOLD];
351         timer = [[NSTimer alloc] initWithFireDate:fireDate interval:SYNC_IDLE_THRESHOLD target:self selector:@selector(lazySync:) userInfo:nil repeats:YES];
352         [fireDate release];
353         [syncRunLoop addTimer:timer forMode:NSDefaultRunLoopMode];
354     }
355 }
356
357 // database functions ---------------------------------------------------------------------------
358 #pragma mark database functions
359
360 -(void)setObject:(id)object forKey:(id)key
361 {
362     WebFileDatabaseOp *op;
363
364     ASSERT(object);
365     ASSERT(key);
366
367     touch = CFAbsoluteTimeGetCurrent();
368
369     LOG(FileDatabaseActivity, "%p - %@", object, key);
370     
371     [mutex lock];
372     
373     [setCache setObject:object forKey:key];
374     op = [[WebFileDatabaseOp alloc] initWithCode:WebFileDatabaseSetObjectOp key:key object:object];
375     [ops addObject:op];
376     [op release];
377     [self setTimer];
378     
379     [mutex unlock];
380 }
381
382 -(void)removeObjectForKey:(id)key
383 {
384     WebFileDatabaseOp *op;
385
386     ASSERT(key);
387
388     touch = CFAbsoluteTimeGetCurrent();
389     
390     [mutex lock];
391     
392     [removeCache addObject:key];
393     op = [[WebFileDatabaseOp alloc] initWithCode:WebFileDatabaseRemoveObjectOp key:key object:nil];
394     [ops addObject:op];
395     [op release];
396     [self setTimer];
397     
398     [mutex unlock];
399 }
400
401 -(void)removeAllObjects
402 {
403     touch = CFAbsoluteTimeGetCurrent();
404
405     [mutex lock];
406     [setCache removeAllObjects];
407     [removeCache removeAllObjects];
408     [ops removeAllObjects];
409     [self close];
410     [[NSFileManager defaultManager] _webkit_backgroundRemoveFileAtPath:path];
411     [self open];
412     [mutex unlock];
413
414     LOG(FileDatabaseActivity, "removeAllObjects");
415 }
416
417 -(id)objectForKey:(id)key
418 {
419     volatile id result;
420     
421     ASSERT(key);
422
423     touch = CFAbsoluteTimeGetCurrent();
424
425     // check caches
426     [mutex lock];
427     if ([removeCache containsObject:key]) {
428         [mutex unlock];
429         return nil;
430     }
431     if ((result = [setCache objectForKey:key])) {
432         [mutex unlock];
433         return result;
434     }
435     [mutex unlock];
436
437     // go to disk
438     char uniqueKey[UniqueFilePathSize];
439     UniqueFilePathForKey(key, uniqueKey);
440     NSString *filePath = [[NSString alloc] initWithFormat:@"%@/%s", path, uniqueKey];
441     NSData *data = [[NSData alloc] initWithContentsOfFile:filePath];
442     NSUnarchiver * volatile unarchiver = nil;
443
444     NS_DURING
445         if (data) {
446             unarchiver = [[NSUnarchiver alloc] initForReadingWithData:data];
447             if (unarchiver) {
448                 id fileKey = [unarchiver decodeObject];
449                 if ([fileKey isEqual:key]) {
450                     id object = [unarchiver decodeObject];
451                     if (object) {
452                         // Decoded objects go away when the unarchiver does, so we need to
453                         // retain this so we can return it to our caller.
454                         result = [[object retain] autorelease];
455                         if (lru) {
456                             // if we can't update the list yet, that's too bad
457                             // but not critically bad
458                             WebLRUFileListTouchFileWithPath(lru, uniqueKey);
459                         }
460                         LOG(FileDatabaseActivity, "read disk cache file - %@", key);
461                     }
462                 }
463             }
464         }
465     NS_HANDLER
466         LOG(FileDatabaseActivity, "cannot unarchive cache file - %@", key);
467         result = nil;
468     NS_ENDHANDLER
469
470     [unarchiver release];
471     [data release];
472     [filePath release];
473
474     LOG(Timing, "getting value for %@ took %f", key, (CFAbsoluteTimeGetCurrent() - touch));
475     
476     return result;
477 }
478
479 -(void)performSetObject:(id)object forKey:(id)key
480 {
481     NSString *filePath;
482     NSMutableData *data;
483     NSDictionary *attributes;
484     NSDictionary *directoryAttributes;
485     NSArchiver *archiver;
486     NSFileManager *defaultManager;
487     char uniqueKey[UniqueFilePathSize];
488     BOOL result;
489
490     ASSERT(object);
491     ASSERT(key);
492
493     UniqueFilePathForKey(key, uniqueKey);
494
495     LOG(FileDatabaseActivity, "%@ - %s", key, uniqueKey);
496
497     data = [NSMutableData data];
498     archiver = [[NSArchiver alloc] initForWritingWithMutableData:data];
499     [archiver encodeObject:key];
500     [archiver encodeObject:object];
501     
502     attributes = [NSDictionary dictionaryWithObjectsAndKeys:
503         NSUserName(), NSFileOwnerAccountName,
504         WebFilePOSIXPermissions, NSFilePosixPermissions,
505         NULL
506     ];
507
508     directoryAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
509         NSUserName(), NSFileOwnerAccountName,
510         WebFileDirectoryPOSIXPermissions, NSFilePosixPermissions,
511         NULL
512     ];
513
514     defaultManager = [NSFileManager defaultManager];
515
516     filePath = [[NSString alloc] initWithFormat:@"%@/%s", path, uniqueKey];
517     attributes = [defaultManager fileAttributesAtPath:filePath traverseLink:YES];
518
519     // update usage and truncate before writing file
520     // this has the effect of _always_ keeping disk usage under sizeLimit by clearing away space in anticipation of the write.
521     WebLRUFileListSetFileData(lru, uniqueKey, [data length], CFAbsoluteTimeGetCurrent());
522     [self _truncateToSizeLimit:[self sizeLimit]];
523
524     result = [defaultManager _webkit_createFileAtPathWithIntermediateDirectories:filePath contents:data attributes:attributes directoryAttributes:directoryAttributes];
525
526     if (!result) {
527         WebLRUFileListRemoveFileWithPath(lru, uniqueKey);
528     }
529
530     [archiver release];
531     [filePath release];    
532 }
533
534 -(void)performRemoveObjectForKey:(id)key
535 {
536     NSString *filePath;
537     char uniqueKey[UniqueFilePathSize];
538     
539     ASSERT(key);
540
541     LOG(FileDatabaseActivity, "%@", key);
542
543     UniqueFilePathForKey(key, uniqueKey);
544     filePath = [[NSString alloc] initWithFormat:@"%@/%s", path, uniqueKey];
545     [[NSFileManager defaultManager] _webkit_removeFileOnlyAtPath:filePath];
546     WebLRUFileListRemoveFileWithPath(lru, uniqueKey);
547     [filePath release];
548 }
549
550 // database management functions ---------------------------------------------------------------------------
551 #pragma mark database management functions
552
553 -(void)open
554 {
555     NSFileManager *manager;
556     NSDictionary *attributes;
557     BOOL isDir;
558     
559     if (!isOpen) {
560         manager = [NSFileManager defaultManager];
561         if ([manager fileExistsAtPath:path isDirectory:&isDir]) {
562             if (isDir) {
563                 isOpen = YES;
564             }
565         }
566         else {
567             attributes = [NSDictionary dictionaryWithObjectsAndKeys:
568                 [NSDate date], @"NSFileModificationDate",
569                 NSUserName(), @"NSFileOwnerAccountName",
570                 WebFileDirectoryPOSIXPermissions, @"NSFilePosixPermissions",
571                 NULL
572             ];
573             
574             isOpen = [manager _webkit_createDirectoryAtPathWithIntermediateDirectories:path attributes:attributes];
575         }
576
577         // remove any leftover turds
578         [manager _webkit_backgroundRemoveLeftoverFiles:path];
579         
580         if (isOpen) {
581             [NSThread detachNewThreadSelector:@selector(_createLRUList:) toTarget:self withObject:nil];
582         }
583     }
584 }
585
586 -(void)close
587 {
588     if (isOpen) {
589         isOpen = NO;
590         if (lru) {
591             WebLRUFileListRelease(lru);
592             lru = NULL;
593         }
594     }
595 }
596
597 -(void)lazySync:(NSTimer *)theTimer
598 {
599     if (!lru) {
600         // wait for lru to finish getting created        
601         return;
602     }
603
604 #ifndef NDEBUG
605     CFTimeInterval mark = CFAbsoluteTimeGetCurrent();
606 #endif
607
608     LOG(FileDatabaseActivity, ">>> BEFORE lazySync\n%@", WebLRUFileListDescription(lru));
609
610     WebFileDatabaseOp *op;
611
612     ASSERT(theTimer);
613
614     while (touch + SYNC_IDLE_THRESHOLD < CFAbsoluteTimeGetCurrent() && [ops count] > 0) {
615         [mutex lock];
616
617         if (timer) {
618             [timer invalidate];
619             [timer autorelease];
620             timer = nil;
621         }
622         
623         op = [ops lastObject];
624         if (op) {
625             [op retain];
626             [ops removeLastObject];
627             [op perform:self];
628             [setCache removeObjectForKey:[op key]];
629             [removeCache removeObject:[op key]];
630             [op release];
631         }
632
633         [mutex unlock];
634     }
635
636     // come back later to finish the work...
637     if ([ops count] > 0) {
638         [mutex lock];
639         [self setTimer];
640         [mutex unlock];
641     }
642
643 #ifndef NDEBUG
644     if (lru)
645         LOG(FileDatabaseActivity, "<<< AFTER lazySync\n%@", WebLRUFileListDescription(lru));
646
647     CFTimeInterval now = CFAbsoluteTimeGetCurrent();
648     LOG(FileDatabaseActivity, "lazySync ran in %.3f secs.", now - mark);
649 #endif
650 }
651
652 -(void)sync
653 {
654     NSArray *array;
655
656     if (!lru) {
657         // wait for lru to finish getting created        
658         return;
659     }
660
661     touch = CFAbsoluteTimeGetCurrent();
662
663     LOG(FileDatabaseActivity, ">>> BEFORE sync\n%@", WebLRUFileListDescription(lru));
664     
665     [mutex lock];
666     array = [ops copy];
667     [ops removeAllObjects];
668     [timer invalidate];
669     [timer autorelease];
670     timer = nil;
671     [setCache removeAllObjects];
672     [removeCache removeAllObjects];
673     [mutex unlock];
674
675     [array makeObjectsPerformSelector:@selector(perform:) withObject:self];
676     [array release];
677
678     LOG(FileDatabaseActivity, "<<< AFTER sync\n%@", WebLRUFileListDescription(lru));
679 }
680
681 -(NSString *)path
682 {
683     return path;
684 }
685
686 -(BOOL)isOpen
687 {
688     return isOpen;
689 }
690
691 -(unsigned)sizeLimit
692 {
693     return sizeLimit;
694 }
695
696 -(unsigned)count
697 {
698     if (lru)
699         return WebLRUFileListCountItems(lru);
700     
701     return 0;
702 }
703
704 -(unsigned)usage
705 {
706     if (lru)
707         return WebLRUFileListGetTotalSize(lru);
708     
709     return 0;
710 }
711
712 -(void)setSizeLimit:(unsigned)limit
713 {
714     sizeLimit = limit;
715     if (limit < [self usage]) {
716         [self _truncateToSizeLimit:limit];
717     }
718 }
719
720
721
722 @end