/// <summary> /// Get a page from cache or from disk (get from cache or from disk) /// </summary> public T GetPage <T>(uint pageID) where T : BasePage { lock (_disk) { var page = _cache.GetPage(pageID); // is not on cache? load from disk if (page == null) { var buffer = _disk.ReadPage(pageID); // if datafile are encrypted, decrypt buffer (header are not encrypted) if (_crypto != null && pageID > 0) { buffer = _crypto.Decrypt(buffer); } page = BasePage.ReadPage(buffer); _cache.AddPage(page); } return((T)page); } }
/// <summary> /// Get journal pages and override all into datafile /// </summary> public void Recovery() { _log.Write(Logger.RECOVERY, "initializing recovery mode"); using (_locker.Write()) { // double check in header need recovery (could be already recover from another thread) var header = BasePage.ReadPage(_disk.ReadPage(0)) as HeaderPage; if (header.Recovery == false) { return; } // read all journal pages foreach (var buffer in _disk.ReadJournal(header.LastPageID)) { // read pageID (first 4 bytes) var pageID = BitConverter.ToUInt32(buffer, 0); _log.Write(Logger.RECOVERY, "recover page #{0:0000}", pageID); // write in stream (encrypt if datafile is encrypted) _disk.WritePage(pageID, _crypto == null || pageID == 0 ? buffer : _crypto.Encrypt(buffer)); } // shrink datafile _disk.ClearJournal(header.LastPageID); } }
/// <summary> /// Initialize LiteEngine using custom disk service implementation and full engine options /// </summary> public LiteEngine(IDiskService disk, string password = null, TimeSpan?timeout = null, int cacheSize = 5000, Logger log = null, bool utcDate = false) { if (disk == null) { throw new ArgumentNullException(nameof(disk)); } _timeout = timeout ?? TimeSpan.FromMinutes(1); _cacheSize = cacheSize; _disk = disk; _log = log ?? new Logger(); _bsonReader = new BsonReader(utcDate); try { // initialize datafile (create) and set log instance _disk.Initialize(_log, password); // lock disk (read mode) before read header var position = _disk.Lock(LockState.Read, _timeout); var buffer = _disk.ReadPage(0); _disk.Unlock(LockState.Read, position); // create header instance from array bytes var header = BasePage.ReadPage(buffer) as HeaderPage; // hash password with sha1 or keep as empty byte[20] var sha1 = password == null ? new byte[20] : AesEncryption.HashSHA1(password); // compare header password with user password even if not passed password (datafile can have password) if (sha1.BinaryCompareTo(header.Password) != 0) { throw LiteException.DatabaseWrongPassword(); } // initialize AES encryptor if (password != null) { _crypto = new AesEncryption(password, header.Salt); } // initialize all services this.InitializeServices(); // if header are marked with recovery, do it now if (header.Recovery) { _trans.Recovery(); } } catch (Exception) { // explicit dispose this.Dispose(); throw; } }
/// <summary> /// Test if cache still valid (if datafile was changed by another process reset cache) /// Returns true if file was changed /// [Thread Safe] /// </summary> private bool DetectDatabaseChanges() { // if disk are exclusive don't need check dirty read if (_disk.IsExclusive) { return(false); } // empty cache? just exit if (_cache.CleanUsed == 0) { return(false); } _log.Write(Logger.CACHE, "checking disk to detect database changes from another process"); // get ChangeID from cache var header = _cache.GetPage(0) as HeaderPage; var changeID = header == null ? 0 : header.ChangeID; // and get header from disk var disk = BasePage.ReadPage(_disk.ReadPage(0)) as HeaderPage; // if disk header are in recovery mode, throw exception to datafile re-open and recovery pages if (disk.Recovery) { _log.Write(Logger.ERROR, "datafile in recovery mode, need re-open database"); throw LiteException.NeedRecover(); } // if header change, clear cache and add new header to cache if (disk.ChangeID != changeID) { _log.Write(Logger.CACHE, "file changed from another process, cleaning all cache pages"); _cache.ClearPages(); _cache.AddPage(disk); return(true); } return(false); }
/// <summary> /// Reduce disk size re-arranging unused spaces. Can change password. If temporary disk was not provided, use MemoryStream temp disk /// </summary> public long Shrink(string password = null, IDiskService tempDisk = null) { var originalSize = _disk.FileLength; // if temp disk are not passed, use memory stream disk using (var temp = tempDisk ?? new StreamDiskService(new MemoryStream())) using (_locker.Write()) using (var engine = new LiteEngine(temp, password)) { // read all collection foreach (var collectionName in this.GetCollectionNames()) { // first create all user indexes (exclude _id index) foreach (var index in this.GetIndexes(collectionName).Where(x => x.Field != "_id")) { engine.EnsureIndex(collectionName, index.Field, index.Unique); } // now copy documents var docs = this.Find(collectionName, Query.All()); engine.InsertBulk(collectionName, docs); // fix collection sequence number var seq = _collections.Get(collectionName).Sequence; engine.Transaction(collectionName, true, (col) => { col.Sequence = seq; engine._pager.SetDirty(col); return(true); }); } // copy user version engine.UserVersion = this.UserVersion; // set current disk size to exact new disk usage _disk.SetLength(temp.FileLength); // read new header page to start copy var header = BasePage.ReadPage(temp.ReadPage(0)) as HeaderPage; // copy (as is) all pages from temp disk to original disk for (uint i = 0; i <= header.LastPageID; i++) { // skip lock page if (i == 1) { continue; } var page = temp.ReadPage(i); _disk.WritePage(i, page); } // create/destroy crypto class if (_crypto != null) { _crypto.Dispose(); } _crypto = password == null ? null : new AesEncryption(password, header.Salt); // initialize all services again (crypto can be changed) this.InitializeServices(); // return how many bytes are reduced return(originalSize - temp.FileLength); } }