/* vim:set ts=2 sw=2 sts=2 et cin: */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Mozilla. * * The Initial Developer of the Original Code is IBM Corporation. * Portions created by IBM Corporation are Copyright (C) 2004 * IBM Corporation. All Rights Reserved. * * Contributor(s): * Darin Fisher * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ // include files for ftruncate (or equivalent) #if defined(XP_UNIX) || defined(XP_BEOS) #include #elif defined(XP_MAC) #include #elif defined(XP_WIN) #include #elif defined(XP_OS2) #define INCL_DOSERRORS #include #else // XXX add necessary include file for ftruncate (or equivalent) #endif #if defined(XP_MAC) #include "pprio.h" #else #include "private/pprio.h" #endif #include "prlong.h" #include "nsCache.h" #include "nsDiskCache.h" #include "nsDiskCacheDeviceSQL.h" #include "nsNetUtil.h" #include "nsAutoPtr.h" #include "nsString.h" #include "nsPrintfCString.h" #include "nsCRT.h" #include "mozIStorageService.h" #include "mozIStorageStatement.h" #include "mozStorageCID.h" #include "nsICacheVisitor.h" #include "nsISeekableStream.h" static const char DISK_CACHE_DEVICE_ID[] = { "disk" }; #define LOG(args) CACHE_LOG_DEBUG(args) /***************************************************************************** * helpers */ static nsresult EnsureDir(nsIFile *dir) { PRBool exists; nsresult rv = dir->Exists(&exists); if (NS_SUCCEEDED(rv) && !exists) rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0700); return rv; } static PRBool DecomposeCacheEntryKey(const nsCString *fullKey, const char **cid, const char **key, nsCString &buf) { buf = *fullKey; PRInt32 colon = buf.FindChar(':'); if (colon == kNotFound) { NS_ERROR("Invalid key"); return PR_FALSE; } buf.SetCharAt('\0', colon); *cid = buf.get(); *key = buf.get() + colon + 1; return PR_TRUE; } class AutoResetStatement { public: AutoResetStatement(mozIStorageStatement *s) : mStatement(s) {} ~AutoResetStatement() { mStatement->Reset(); } mozIStorageStatement *operator->() { return mStatement; } private: mozIStorageStatement *mStatement; }; /****************************************************************************** * nsDiskCache *****************************************************************************/ #define DCACHE_HASH_MAX LL_MAXINT #define DCACHE_HASH_BITS 64 /** * nsDiskCache::Hash(const char * key) * * This algorithm of this method implies nsDiskCacheRecords will be stored * in a certain order on disk. If the algorithm changes, existing cache * map files may become invalid, and therefore the kCurrentVersion needs * to be revised. */ static PRUint64 DCacheHash(const char * key) { PRUint64 h = 0; for (const PRUint8* s = (PRUint8*) key; *s != '\0'; ++s) h = (h >> (DCACHE_HASH_BITS - 4)) ^ (h << 4) ^ *s; return (h == 0 ? DCACHE_HASH_MAX : h); } nsresult nsDiskCache::Truncate(PRFileDesc * fd, PRUint32 newEOF) { // use modified SetEOF from nsFileStreams::SetEOF() #if defined(XP_UNIX) || defined(XP_BEOS) if (ftruncate(PR_FileDesc2NativeHandle(fd), newEOF) != 0) { NS_ERROR("ftruncate failed"); return NS_ERROR_FAILURE; } #elif defined(XP_MAC) if (::SetEOF(PR_FileDesc2NativeHandle(fd), newEOF) != 0) { NS_ERROR("SetEOF failed"); return NS_ERROR_FAILURE; } #elif defined(XP_WIN) PRInt32 cnt = PR_Seek(fd, newEOF, PR_SEEK_SET); if (cnt == -1) return NS_ERROR_FAILURE; if (!SetEndOfFile((HANDLE) PR_FileDesc2NativeHandle(fd))) { NS_ERROR("SetEndOfFile failed"); return NS_ERROR_FAILURE; } #elif defined(XP_OS2) if (DosSetFileSize((HFILE) PR_FileDesc2NativeHandle(fd), newEOF) != NO_ERROR) { NS_ERROR("DosSetFileSize failed"); return NS_ERROR_FAILURE; } #else // add implementations for other platforms here #endif return NS_OK; } /****************************************************************************** * nsDiskCacheDeviceInfo */ class nsDiskCacheDeviceInfo : public nsICacheDeviceInfo { public: NS_DECL_ISUPPORTS NS_DECL_NSICACHEDEVICEINFO nsDiskCacheDeviceInfo(nsDiskCacheDevice* device) : mDevice(device) {} private: nsDiskCacheDevice* mDevice; }; NS_IMPL_ISUPPORTS1(nsDiskCacheDeviceInfo, nsICacheDeviceInfo) NS_IMETHODIMP nsDiskCacheDeviceInfo::GetDescription(char **aDescription) { *aDescription = nsCRT::strdup("Disk cache device"); return *aDescription ? NS_OK : NS_ERROR_OUT_OF_MEMORY; } NS_IMETHODIMP nsDiskCacheDeviceInfo::GetUsageReport(char ** usageReport) { nsCAutoString buffer; buffer.AppendLiteral("\n\nCache Directory:\n "); nsILocalFile *cacheDir = mDevice->CacheDirectory(); if (!cacheDir) return NS_OK; nsAutoString path; nsresult rv = cacheDir->GetPath(path); if (NS_SUCCEEDED(rv)) AppendUTF16toUTF8(path, buffer); else buffer.AppendLiteral("directory unavailable"); buffer.AppendLiteral("\n\n"); *usageReport = ToNewCString(buffer); if (!*usageReport) return NS_ERROR_OUT_OF_MEMORY; return NS_OK; } NS_IMETHODIMP nsDiskCacheDeviceInfo::GetEntryCount(PRUint32 *aEntryCount) { *aEntryCount = mDevice->EntryCount(); return NS_OK; } NS_IMETHODIMP nsDiskCacheDeviceInfo::GetTotalSize(PRUint32 *aTotalSize) { *aTotalSize = mDevice->CacheSize(); return NS_OK; } NS_IMETHODIMP nsDiskCacheDeviceInfo::GetMaximumSize(PRUint32 *aMaximumSize) { *aMaximumSize = mDevice->CacheCapacity(); return NS_OK; } /****************************************************************************** * nsDiskCacheBinding */ class nsDiskCacheBinding : public nsISupports { public: NS_DECL_ISUPPORTS static nsDiskCacheBinding * Create(nsIFile *cacheDir, const char *key, int generation); nsCOMPtr mDataFile; int mGeneration; }; NS_IMPL_THREADSAFE_ISUPPORTS0(nsDiskCacheBinding) nsDiskCacheBinding * nsDiskCacheBinding::Create(nsIFile *cacheDir, const char *key, int generation) { nsCOMPtr file; cacheDir->Clone(getter_AddRefs(file)); if (!file) return nsnull; PRUint64 hash = DCacheHash(key); PRUint32 dir1 = (PRUint32) (hash & 0x0F); PRUint32 dir2 = (PRUint32)((hash & 0xF0) >> 4); hash >>= 8; // XXX we might want to create these directories up-front file->AppendNative(nsPrintfCString("%X", dir1)); file->Create(nsIFile::DIRECTORY_TYPE, 00700); file->AppendNative(nsPrintfCString("%X", dir2)); file->Create(nsIFile::DIRECTORY_TYPE, 00700); nsresult rv; char leaf[64]; if (generation == -1) { file->AppendNative(NS_LITERAL_CSTRING("placeholder")); for (generation = 0; ; ++generation) { PR_snprintf(leaf, sizeof(leaf), "%014llX-%X", hash, generation); rv = file->SetNativeLeafName(nsDependentCString(leaf)); if (NS_FAILED(rv)) return nsnull; rv = file->Create(nsIFile::NORMAL_FILE_TYPE, 00600); if (NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS) return nsnull; if (NS_SUCCEEDED(rv)) break; } } else { PR_snprintf(leaf, sizeof(leaf), "%014llX-%X", hash, generation); rv = file->AppendNative(nsDependentCString(leaf)); if (NS_FAILED(rv)) return nsnull; } nsDiskCacheBinding *binding = new nsDiskCacheBinding; if (!binding) return nsnull; binding->mDataFile.swap(file); binding->mGeneration = generation; return binding; } // helper function for directly exposing the same data file binding // path algorithm used in nsDiskCacheBinding::Create static nsresult GetCacheDataFile(nsIFile *cacheDir, const char *cid, const char *key, int generation, nsCOMPtr &file) { cacheDir->Clone(getter_AddRefs(file)); if (!file) return NS_ERROR_OUT_OF_MEMORY; nsCAutoString fullKey; fullKey.Append(cid); fullKey.Append(':'); fullKey.Append(key); PRUint64 hash = DCacheHash(fullKey.get()); PRUint32 dir1 = (PRUint32) (hash & 0x0F); PRUint32 dir2 = (PRUint32)((hash & 0xF0) >> 4); hash >>= 8; file->AppendNative(nsPrintfCString("%X", dir1)); file->AppendNative(nsPrintfCString("%X", dir2)); char leaf[64]; PR_snprintf(leaf, sizeof(leaf), "%014llX-%X", hash, generation); return file->AppendNative(nsDependentCString(leaf)); } /****************************************************************************** * nsDiskCacheRecord */ struct nsDiskCacheRecord { const char *clientID; const char *key; const PRUint8 *metaData; PRUint32 metaDataLen; PRInt32 generation; PRInt32 flags; PRInt32 dataSize; PRInt32 fetchCount; PRInt64 lastFetched; PRInt64 lastModified; PRInt64 expirationTime; }; static nsCacheEntry * CreateCacheEntry(nsDiskCacheDevice *device, const nsCString *fullKey, const nsDiskCacheRecord &rec) { if (rec.flags != 0) { LOG(("refusing to load busy entry\n")); return nsnull; } nsCacheEntry *entry; nsresult rv = nsCacheEntry::Create(fullKey->get(), // XXX enable sharing nsICache::STREAM_BASED, nsICache::STORE_ON_DISK, device, &entry); if (NS_FAILED(rv)) return nsnull; entry->SetFetchCount((PRUint32) rec.fetchCount); entry->SetLastFetched(SecondsFromPRTime(rec.lastFetched)); entry->SetLastModified(SecondsFromPRTime(rec.lastModified)); entry->SetExpirationTime(SecondsFromPRTime(rec.expirationTime)); entry->SetDataSize((PRUint32) rec.dataSize); entry->UnflattenMetaData((const char *) rec.metaData, rec.metaDataLen); // create a binding object for this entry nsDiskCacheBinding *binding = nsDiskCacheBinding::Create(device->CacheDirectory(), fullKey->get(), rec.generation); if (!binding) { delete entry; return nsnull; } entry->SetData(binding); return entry; } /****************************************************************************** * nsDiskCacheEntryInfo */ class nsDiskCacheEntryInfo : public nsICacheEntryInfo { public: NS_DECL_ISUPPORTS NS_DECL_NSICACHEENTRYINFO nsDiskCacheRecord *mRec; }; NS_IMPL_ISUPPORTS1(nsDiskCacheEntryInfo, nsICacheEntryInfo) NS_IMETHODIMP nsDiskCacheEntryInfo::GetClientID(char **result) { *result = nsCRT::strdup(mRec->clientID); return *result ? NS_OK : NS_ERROR_OUT_OF_MEMORY; } NS_IMETHODIMP nsDiskCacheEntryInfo::GetDeviceID(char ** deviceID) { *deviceID = nsCRT::strdup(DISK_CACHE_DEVICE_ID); return *deviceID ? NS_OK : NS_ERROR_OUT_OF_MEMORY; } NS_IMETHODIMP nsDiskCacheEntryInfo::GetKey(nsACString &clientKey) { clientKey.Assign(mRec->key); return NS_OK; } NS_IMETHODIMP nsDiskCacheEntryInfo::GetFetchCount(PRInt32 *aFetchCount) { *aFetchCount = mRec->fetchCount; return NS_OK; } NS_IMETHODIMP nsDiskCacheEntryInfo::GetLastFetched(PRUint32 *aLastFetched) { *aLastFetched = SecondsFromPRTime(mRec->lastFetched); return NS_OK; } NS_IMETHODIMP nsDiskCacheEntryInfo::GetLastModified(PRUint32 *aLastModified) { *aLastModified = SecondsFromPRTime(mRec->lastModified); return NS_OK; } NS_IMETHODIMP nsDiskCacheEntryInfo::GetExpirationTime(PRUint32 *aExpirationTime) { *aExpirationTime = SecondsFromPRTime(mRec->expirationTime); return NS_OK; } NS_IMETHODIMP nsDiskCacheEntryInfo::IsStreamBased(PRBool *aStreamBased) { *aStreamBased = PR_TRUE; return NS_OK; } NS_IMETHODIMP nsDiskCacheEntryInfo::GetDataSize(PRUint32 *aDataSize) { *aDataSize = mRec->dataSize; return NS_OK; } /****************************************************************************** * nsDiskCacheDevice */ nsDiskCacheDevice::nsDiskCacheDevice() : mDB(nsnull) , mCacheCapacity(0) , mDeltaCounter(0) { } nsDiskCacheDevice::~nsDiskCacheDevice() { Shutdown(); } PRUint32 nsDiskCacheDevice::CacheSize() { AutoResetStatement statement(mStatement_CacheSize); PRBool hasRows; nsresult rv = statement->ExecuteStep(&hasRows); NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hasRows, 0); return (PRUint32) statement->AsInt32(0); } PRUint32 nsDiskCacheDevice::EntryCount() { AutoResetStatement statement(mStatement_EntryCount); PRBool hasRows; nsresult rv = statement->ExecuteStep(&hasRows); NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hasRows, 0); return (PRUint32) statement->AsInt32(0); } nsresult nsDiskCacheDevice::UpdateEntry(nsCacheEntry *entry) { // Decompose the key into "ClientID" and "Key" nsCAutoString keyBuf; const char *cid, *key; if (!DecomposeCacheEntryKey(entry->Key(), &cid, &key, keyBuf)) return NS_ERROR_UNEXPECTED; nsCString metaDataBuf; PRUint32 mdSize = entry->MetaDataSize(); if (!EnsureStringLength(metaDataBuf, mdSize)) return NS_ERROR_OUT_OF_MEMORY; char *md = metaDataBuf.BeginWriting(); entry->FlattenMetaData(md, mdSize); nsDiskCacheRecord rec; rec.metaData = (const PRUint8 *) md; rec.metaDataLen = mdSize; rec.flags = 0; // mark entry as inactive rec.dataSize = entry->DataSize(); rec.fetchCount = entry->FetchCount(); rec.lastFetched = PRTimeFromSeconds(entry->LastFetched()); rec.lastModified = PRTimeFromSeconds(entry->LastModified()); rec.expirationTime = PRTimeFromSeconds(entry->ExpirationTime()); AutoResetStatement statement(mStatement_UpdateEntry); nsresult rv; rv = statement->BindDataParameter(0, rec.metaData, rec.metaDataLen); rv |= statement->BindInt32Parameter(1, rec.flags); rv |= statement->BindInt32Parameter(2, rec.dataSize); rv |= statement->BindInt32Parameter(3, rec.fetchCount); rv |= statement->BindInt64Parameter(4, rec.lastFetched); rv |= statement->BindInt64Parameter(5, rec.lastModified); rv |= statement->BindInt64Parameter(6, rec.expirationTime); rv |= statement->BindCStringParameter(7, cid); rv |= statement->BindCStringParameter(8, key); NS_ENSURE_SUCCESS(rv, rv); PRBool hasRows; rv = statement->ExecuteStep(&hasRows); NS_ENSURE_SUCCESS(rv, rv); NS_ASSERTION(!hasRows, "UPDATE should not result in output"); return rv; } nsresult nsDiskCacheDevice::UpdateEntrySize(nsCacheEntry *entry, PRUint32 newSize) { // Decompose the key into "ClientID" and "Key" nsCAutoString keyBuf; const char *cid, *key; if (!DecomposeCacheEntryKey(entry->Key(), &cid, &key, keyBuf)) return NS_ERROR_UNEXPECTED; AutoResetStatement statement(mStatement_UpdateEntrySize); nsresult rv; rv = statement->BindInt32Parameter(0, newSize); rv |= statement->BindCStringParameter(1, cid); rv |= statement->BindCStringParameter(2, key); NS_ENSURE_SUCCESS(rv, rv); PRBool hasRows; rv = statement->ExecuteStep(&hasRows); NS_ENSURE_SUCCESS(rv, rv); NS_ASSERTION(!hasRows, "UPDATE should not result in output"); return rv; } nsresult nsDiskCacheDevice::DeleteEntry(nsCacheEntry *entry, PRBool deleteData) { if (deleteData) { nsresult rv = DeleteData(entry); if (NS_FAILED(rv)) return rv; } // Decompose the key into "ClientID" and "Key" nsCAutoString keyBuf; const char *cid, *key; if (!DecomposeCacheEntryKey(entry->Key(), &cid, &key, keyBuf)) return NS_ERROR_UNEXPECTED; AutoResetStatement statement(mStatement_DeleteEntry); nsresult rv; rv = statement->BindCStringParameter(0, cid); rv |= statement->BindCStringParameter(1, key); NS_ENSURE_SUCCESS(rv, rv); PRBool hasRows; rv = statement->ExecuteStep(&hasRows); NS_ENSURE_SUCCESS(rv, rv); NS_ASSERTION(!hasRows, "DELETE should not result in output"); return rv; } nsresult nsDiskCacheDevice::DeleteData(nsCacheEntry *entry) { nsDiskCacheBinding *binding = (nsDiskCacheBinding *) entry->Data(); NS_ENSURE_STATE(binding); return binding->mDataFile->Remove(PR_FALSE); } nsresult nsDiskCacheDevice::EnableEvictionObserver() { #if 0 // use CreateTrigger .. maybe do this only once, and have a member // variable to control whether or not it is active. int res = sqlite3_exec(mDB, "CREATE TEMP TRIGGER cache_on_delete AFTER DELETE" " ON moz_cache FOR EACH ROW BEGIN SELECT" " cache_eviction_observer(" " OLD.clientID, OLD.key, OLD.generation);" " END;", NULL, NULL, NULL); NS_ENSURE_SQLITE_RESULT(res, NS_ERROR_UNEXPECTED); #endif return NS_OK; } nsresult nsDiskCacheDevice::DisableEvictionObserver() { #if 0 int res = sqlite3_exec(mDB, "DROP TRIGGER cache_on_delete;", NULL, NULL, NULL); NS_ENSURE_SQLITE_RESULT(res, NS_ERROR_UNEXPECTED); #endif return NS_OK; } #if 0 /* static */ void nsDiskCacheDevice::EvictionObserver(sqlite3_context *ctx, int narg, sqlite3_value **values) { LOG(("nsDiskCacheDevice::EvictionObserver\n")); nsDiskCacheDevice *device = (nsDiskCacheDevice *) sqlite3_user_data(ctx); NS_ASSERTION(narg == 3, "unexpected number of arguments"); const char *cid = (const char *) sqlite3_value_text(values[0]); const char *key = (const char *) sqlite3_value_text(values[1]); int generation = sqlite3_value_int(values[2]); nsCOMPtr file; nsresult rv = GetCacheDataFile(device->CacheDirectory(), cid, key, generation, file); if (NS_FAILED(rv)) { LOG(("GetCacheDataFile [cid=%s key=%s generation=%d] failed [rv=%x]!\n", cid, key, generation, rv)); return; } #if defined(PR_LOGGING) nsCAutoString path; file->GetNativePath(path); LOG((" removing %s\n", path.get())); #endif file->Remove(PR_FALSE); } #endif /** * nsCacheDevice implementation */ nsresult nsDiskCacheDevice::Init() { NS_ENSURE_TRUE(!mDB, NS_ERROR_ALREADY_INITIALIZED); // SetCacheParentDirectory must have been called NS_ENSURE_TRUE(mCacheDirectory, NS_ERROR_UNEXPECTED); // make sure the cache directory exists nsresult rv = EnsureDir(mCacheDirectory); NS_ENSURE_SUCCESS(rv, rv); // build path to index file nsCOMPtr indexFile; rv = mCacheDirectory->Clone(getter_AddRefs(indexFile)); NS_ENSURE_SUCCESS(rv, rv); rv = indexFile->AppendNative(NS_LITERAL_CSTRING("index.db")); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr ss = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); rv = ss->OpenDatabase(indexFile, getter_AddRefs(mDB)); NS_ENSURE_SUCCESS(rv, rv); mDB->ExecuteSimpleSQL(NS_LITERAL_CSTRING("PRAGMA synchronous = OFF;")); // XXX ... other initialization steps // XXX in the future we may wish to verify the schema for moz_cache // perhaps using "PRAGMA table_info" ? // build the table // // "Generation" is the data file generation number. // "Flags" is a bit-field indicating the state of the entry. // mDB->ExecuteSimpleSQL( NS_LITERAL_CSTRING("CREATE TABLE moz_cache (\n" " ClientID TEXT,\n" " Key TEXT,\n" " MetaData BLOB,\n" " Generation INTEGER,\n" " Flags INTEGER,\n" " DataSize INTEGER,\n" " FetchCount INTEGER,\n" " LastFetched INTEGER,\n" " LastModified INTEGER,\n" " ExpirationTime INTEGER\n" ");\n")); // maybe the table already exists, so don't bother checking for errors. mDB->ExecuteSimpleSQL( NS_LITERAL_CSTRING("CREATE UNIQUE INDEX moz_cache_index" " ON moz_cache (ClientID, Key);")); // maybe the index already exists, so don't bother checking for errors. #if 0 // create cache_eviction_observer res = sqlite3_create_function(mDB, "cache_eviction_observer", 3, SQLITE_UTF8, this, nsDiskCacheDevice::EvictionObserver, NULL, NULL); if (res != SQLITE_OK) LOG(("sqlite3_create_function failed [res=%d]\n", res)); #endif // create all (most) of our statements up front struct { nsCOMPtr &statement; const char *sql; } prepared[] = { { mStatement_CacheSize, "SELECT Sum(DataSize) from moz_cache;" }, { mStatement_EntryCount, "SELECT count(*) from moz_cache;" }, { mStatement_UpdateEntry, "UPDATE moz_cache SET MetaData = ?, Flags = ?, DataSize = ?, FetchCount = ?, LastFetched = ?, LastModified = ?, ExpirationTime = ? WHERE ClientID = ? AND Key = ?;" }, { mStatement_UpdateEntrySize, "UPDATE moz_cache SET DataSize = ? WHERE ClientID = ? AND Key = ?;" }, { mStatement_DeleteEntry, "DELETE FROM moz_cache WHERE ClientID = ? AND Key = ?;" }, { mStatement_FindEntry, "SELECT MetaData, Generation, Flags, DataSize, FetchCount, LastFetched, LastModified, ExpirationTime FROM moz_cache WHERE ClientID = ? AND Key = ?;" }, { mStatement_BindEntry, "INSERT INTO moz_cache VALUES(?,?,?,?,?,?,?,?,?,?);" } }; for (PRUint32 i=0; iCreateStatement(nsDependentCString(prepared[i].sql), getter_AddRefs(prepared[i].statement)); } NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult nsDiskCacheDevice::Shutdown() { NS_ENSURE_TRUE(mDB, NS_ERROR_NOT_INITIALIZED); #if 0 int res; // delete cache_eviction_observer res = sqlite3_create_function(mDB, "cache_eviction_observer", 3, SQLITE_UTF8, NULL, NULL, NULL, NULL); if (res != SQLITE_OK) LOG(("sqlite3_create_function failed [res=%d]\n", res)); res = sqlite3_close(mDB); NS_ENSURE_SQLITE_RESULT(res, NS_ERROR_UNEXPECTED); mDB = nsnull; #endif mDB = 0; return NS_OK; } const char * nsDiskCacheDevice::GetDeviceID() { return DISK_CACHE_DEVICE_ID; } nsCacheEntry * nsDiskCacheDevice::FindEntry(nsCString *fullKey, PRBool *collision) { LOG(("nsDiskCacheDevice::FindEntry [key=%s]\n", fullKey->get())); // SELECT * FROM moz_cache WHERE key = ? // Decompose the key into "ClientID" and "Key" nsCAutoString keyBuf; const char *cid, *key; if (!DecomposeCacheEntryKey(fullKey, &cid, &key, keyBuf)) return nsnull; AutoResetStatement statement(mStatement_FindEntry); nsresult rv; rv = statement->BindCStringParameter(0, cid); rv |= statement->BindCStringParameter(1, key); NS_ENSURE_SUCCESS(rv, nsnull); PRBool hasRows; rv = statement->ExecuteStep(&hasRows); if (NS_FAILED(rv) || !hasRows) return nsnull; // entry not found nsDiskCacheRecord rec; statement->AsSharedBlob(0, (const void **) &rec.metaData, &rec.metaDataLen); rec.generation = statement->AsInt32(1); rec.flags = statement->AsInt32(2); rec.dataSize = statement->AsInt32(3); rec.fetchCount = statement->AsInt32(4); rec.lastFetched = statement->AsInt64(5); rec.lastModified = statement->AsInt64(6); rec.expirationTime = statement->AsInt64(7); LOG(("entry: [%u %d %d %d %d %lld %lld %lld]\n", rec.metaDataLen, rec.generation, rec.flags, rec.dataSize, rec.fetchCount, rec.lastFetched, rec.lastModified, rec.expirationTime)); return CreateCacheEntry(this, fullKey, rec); // XXX we should verify that the corresponding data file exists, // and if not, then we should delete this row and return null. } nsresult nsDiskCacheDevice::DeactivateEntry(nsCacheEntry *entry) { LOG(("nsDiskCacheDevice::DeactivateEntry [key=%s]\n", entry->Key()->get())); // This method is called to inform us that the nsCacheEntry object is going // away. We should persist anything that needs to be persisted, or if the // entry is doomed, we can go ahead and clear its storage. if (entry->IsDoomed()) { // remove corresponding row and file if they exist // the row should have been removed in DoomEntry... we could assert that // that happened. otherwise, all we have to do here is delete the file // on disk. DeleteData(entry); } else { // UPDATE the database row // XXX Assumption: the row already exists because it was either created // with a call to BindEntry or it was there when we called FindEntry. UpdateEntry(entry); } delete entry; return NS_OK; } nsresult nsDiskCacheDevice::BindEntry(nsCacheEntry *entry) { LOG(("nsDiskCacheDevice::BindEntry [key=%s]\n", entry->Key()->get())); NS_ENSURE_STATE(!entry->Data()); // This method is called to inform us that we have a new entry. The entry // may collide with an existing entry in our DB, but if that happens we can // assume that the entry is not being used. // INSERT the database row // XXX Assumption: if the row already exists, then FindEntry would have // returned it. if that entry was doomed, then DoomEntry would have removed // it from the table. so, we should always have to insert at this point. // Decompose the key into "ClientID" and "Key" nsCAutoString keyBuf; const char *cid, *key; if (!DecomposeCacheEntryKey(entry->Key(), &cid, &key, keyBuf)) return NS_ERROR_UNEXPECTED; // create binding, pick best generation number nsRefPtr binding = nsDiskCacheBinding::Create(mCacheDirectory, entry->Key()->get(), -1); if (!binding) return NS_ERROR_OUT_OF_MEMORY; nsDiskCacheRecord rec; rec.clientID = cid; rec.key = key; rec.metaData = NULL; // don't write any metadata now. rec.metaDataLen = 0; rec.generation = binding->mGeneration; rec.flags = 0x1; // mark entry as active, we'll reset this in DeactivateEntry rec.dataSize = 0; rec.fetchCount = entry->FetchCount(); rec.lastFetched = PRTimeFromSeconds(entry->LastFetched()); rec.lastModified = PRTimeFromSeconds(entry->LastModified()); rec.expirationTime = PRTimeFromSeconds(entry->ExpirationTime()); AutoResetStatement statement(mStatement_BindEntry); nsresult rv; rv = statement->BindCStringParameter(0, rec.clientID); rv |= statement->BindCStringParameter(1, rec.key); rv |= statement->BindDataParameter(2, rec.metaData, rec.metaDataLen); rv |= statement->BindInt32Parameter(3, rec.generation); rv |= statement->BindInt32Parameter(4, rec.flags); rv |= statement->BindInt32Parameter(5, rec.dataSize); rv |= statement->BindInt32Parameter(6, rec.fetchCount); rv |= statement->BindInt64Parameter(7, rec.lastFetched); rv |= statement->BindInt64Parameter(8, rec.lastModified); rv |= statement->BindInt64Parameter(9, rec.expirationTime); NS_ENSURE_SUCCESS(rv, rv); PRBool hasRows; rv = statement->ExecuteStep(&hasRows); NS_ENSURE_SUCCESS(rv, rv); NS_ASSERTION(!hasRows, "INSERT should not result in output"); entry->SetData(binding); return NS_OK; } void nsDiskCacheDevice::DoomEntry(nsCacheEntry *entry) { LOG(("nsDiskCacheDevice::DoomEntry [key=%s]\n", entry->Key()->get())); // This method is called to inform us that we should mark the entry to be // deleted when it is no longer in use. // We can go ahead and delete the corresponding row in our table, // but we must not delete the file on disk until we are deactivated. DeleteEntry(entry, PR_FALSE); } nsresult nsDiskCacheDevice::OpenInputStreamForEntry(nsCacheEntry *entry, nsCacheAccessMode mode, PRUint32 offset, nsIInputStream **result) { LOG(("nsDiskCacheDevice::OpenInputStreamForEntry [key=%s]\n", entry->Key()->get())); *result = nsnull; NS_ENSURE_TRUE(offset < entry->DataSize(), NS_ERROR_INVALID_ARG); // return an input stream to the entry's data file. the stream // may be read on a background thread. nsDiskCacheBinding *binding = (nsDiskCacheBinding *) entry->Data(); NS_ENSURE_STATE(binding); nsCOMPtr in; NS_NewLocalFileInputStream(getter_AddRefs(in), binding->mDataFile, PR_RDONLY); if (!in) return NS_ERROR_UNEXPECTED; // respect |offset| param if (offset != 0) { nsCOMPtr seekable = do_QueryInterface(in); NS_ENSURE_TRUE(seekable, NS_ERROR_UNEXPECTED); seekable->Seek(nsISeekableStream::NS_SEEK_SET, offset); } in.swap(*result); return NS_OK; } nsresult nsDiskCacheDevice::OpenOutputStreamForEntry(nsCacheEntry *entry, nsCacheAccessMode mode, PRUint32 offset, nsIOutputStream **result) { LOG(("nsDiskCacheDevice::OpenOutputStreamForEntry [key=%s]\n", entry->Key()->get())); *result = nsnull; NS_ENSURE_TRUE(offset <= entry->DataSize(), NS_ERROR_INVALID_ARG); // return an output stream to the entry's data file. we can assume // that the output stream will only be used on the main thread. nsDiskCacheBinding *binding = (nsDiskCacheBinding *) entry->Data(); NS_ENSURE_STATE(binding); nsCOMPtr out; NS_NewLocalFileOutputStream(getter_AddRefs(out), binding->mDataFile, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, 00600); if (!out) return NS_ERROR_UNEXPECTED; // respect |offset| param nsCOMPtr seekable = do_QueryInterface(out); NS_ENSURE_TRUE(seekable, NS_ERROR_UNEXPECTED); if (offset != 0) seekable->Seek(nsISeekableStream::NS_SEEK_SET, offset); // truncate the file at the given offset seekable->SetEOF(); nsCOMPtr bufferedOut; NS_NewBufferedOutputStream(getter_AddRefs(bufferedOut), out, 16 * 1024); if (!bufferedOut) return NS_ERROR_UNEXPECTED; bufferedOut.swap(*result); return NS_OK; } nsresult nsDiskCacheDevice::GetFileForEntry(nsCacheEntry *entry, nsIFile **result) { LOG(("nsDiskCacheDevice::GetFileForEntry [key=%s]\n", entry->Key()->get())); nsDiskCacheBinding *binding = (nsDiskCacheBinding *) entry->Data(); NS_ENSURE_STATE(binding); NS_IF_ADDREF(*result = binding->mDataFile); return NS_OK; } nsresult nsDiskCacheDevice::OnDataSizeChange(nsCacheEntry *entry, PRInt32 deltaSize) { LOG(("nsDiskCacheDevice::OnDataSizeChange [key=%s delta=%d]\n", entry->Key()->get(), deltaSize)); // called to notify us of an impending change in the total size of the // specified entry. we may wish to trigger an eviction cycle at this point. // update the DataSize attribute for this entry /* PRUint32 oldSize = entry->DataSize(); NS_ASSERTION(deltaSize >= 0 || PRInt32(oldSize) + deltaSize >= 0, "oops"); PRUint32 newSize = PRInt32(oldSize) + deltaSize; UpdateEntrySize(entry, newSize); */ const PRInt32 DELTA_THRESHOLD = 1<<14; // 16k mDeltaCounter += deltaSize; // this may go negative // run eviction cycle if (mDeltaCounter >= DELTA_THRESHOLD) { PRUint32 targetCap, delta = mDeltaCounter; if (delta <= mCacheCapacity) targetCap = mCacheCapacity - delta; else targetCap = 0; EvictDiskCacheEntries(targetCap); mDeltaCounter = 0; // reset counter } return NS_OK; } nsresult nsDiskCacheDevice::Visit(nsICacheVisitor *visitor) { NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); // called to enumerate the disk cache. nsCOMPtr deviceInfo = new nsDiskCacheDeviceInfo(this); PRBool keepGoing; nsresult rv = visitor->VisitDevice(DISK_CACHE_DEVICE_ID, deviceInfo, &keepGoing); if (NS_FAILED(rv)) return rv; if (!keepGoing) return NS_OK; // SELECT * from moz_cache; nsDiskCacheRecord rec; nsRefPtr info = new nsDiskCacheEntryInfo; if (!info) return NS_ERROR_OUT_OF_MEMORY; info->mRec = &rec; // XXX may want to list columns explicitly nsCOMPtr statement; rv = mDB->CreateStatement( NS_LITERAL_CSTRING("SELECT * FROM moz_cache;"), getter_AddRefs(statement)); NS_ENSURE_SUCCESS(rv, rv); PRBool hasRows; for (;;) { rv = statement->ExecuteStep(&hasRows); if (NS_FAILED(rv) || !hasRows) break; rec.clientID = statement->AsSharedCString(0, NULL); rec.key = statement->AsSharedCString(1, NULL); statement->AsSharedBlob(2, (const void **) &rec.metaData, &rec.metaDataLen); rec.generation = statement->AsInt32(3); rec.flags = statement->AsInt32(4); rec.dataSize = statement->AsInt32(5); rec.fetchCount = statement->AsInt32(6); rec.lastFetched = statement->AsInt64(7); rec.lastModified = statement->AsInt64(8); rec.expirationTime = statement->AsInt64(9); PRBool keepGoing; rv = visitor->VisitEntry(DISK_CACHE_DEVICE_ID, info, &keepGoing); if (NS_FAILED(rv) || !keepGoing) break; } info->mRec = nsnull; return NS_OK; } nsresult nsDiskCacheDevice::EvictEntries(const char *clientID) { LOG(("nsDiskCacheDevice::EvictEntries [cid=%s]\n", clientID ? clientID : "")); // called to evict all entries matching the given clientID. // need trigger to fire user defined function after a row is deleted // so we can delete the corresponding data file. nsresult rv = EnableEvictionObserver(); NS_ENSURE_SUCCESS(rv, rv); // hook up our eviction observer const char *deleteCmd; if (clientID) { deleteCmd = PR_smprintf("DELETE FROM moz_cache WHERE ClientID=%q AND Flags=0;", clientID); if (!deleteCmd) return NS_ERROR_OUT_OF_MEMORY; } else { deleteCmd = "DELETE FROM moz_cache WHERE Flags = 0;"; } rv = mDB->ExecuteSimpleSQL(nsDependentCString(deleteCmd)); if (clientID) PR_smprintf_free((char *) deleteCmd); NS_ENSURE_SUCCESS(rv, rv); DisableEvictionObserver(); return NS_OK; } nsresult nsDiskCacheDevice::EvictDiskCacheEntries(PRUint32 desiredCapacity) { LOG(("nsDiskCacheDevice::EvictDiskCacheEntries [goal=%u delta=%d]\n", desiredCapacity, mCacheCapacity - desiredCapacity)); // need trigger to fire user defined function after a row is deleted // so we can delete the corresponding data file. // BEGIN // while ("SELECT Sum(DataSize) FROM moz_cache;" > desiredCapacity) // DELETE FROM moz_cache WHERE Min(LastFetched); // END nsresult rv = EnableEvictionObserver(); NS_ENSURE_SUCCESS(rv, rv); PRUint32 lastCacheSize = PR_UINT32_MAX, cacheSize; for (;;) { cacheSize = CacheSize(); if (cacheSize <= desiredCapacity) break; if (cacheSize == lastCacheSize) { LOG(("unable to reduce cache size to target capacity!\n")); break; } rv = mDB->ExecuteSimpleSQL( NS_LITERAL_CSTRING("DELETE FROM moz_cache WHERE LastFetched IN (" " SELECT Min(LastFetched) FROM moz_cache" " WHERE Flags=0);")); if (NS_FAILED(rv)) { LOG(("failure while deleting Min(LastFetched)\n")); break; } lastCacheSize = cacheSize; } DisableEvictionObserver(); return NS_OK; } /** * Preference accessors */ void nsDiskCacheDevice::SetCacheParentDirectory(nsILocalFile *parentDir) { if (Initialized()) { NS_ERROR("cannot switch cache directory once initialized"); return; } if (!parentDir) { mCacheDirectory = nsnull; return; } // ensure parent directory exists nsresult rv = EnsureDir(parentDir); if (NS_FAILED(rv)) { NS_WARNING("unable to create parent directory"); return; } // cache dir may not exist, but that's ok nsCOMPtr dir; rv = parentDir->Clone(getter_AddRefs(dir)); if (NS_FAILED(rv)) return; rv = dir->AppendNative(NS_LITERAL_CSTRING("cache_sql")); if (NS_FAILED(rv)) return; mCacheDirectory = do_QueryInterface(dir); } void nsDiskCacheDevice::SetCapacity(PRUint32 capacity) { mCacheCapacity = capacity * 1024; if (Initialized()) EvictDiskCacheEntries(mCacheCapacity); }