/// <exception cref="System.Exception"></exception> public virtual void TestBlobStoreWriterForBody() { BlobStore attachments = database.GetAttachments(); InputStream attachmentStream = GetAsset("attachment.png"); BlobStoreWriter blobStoreWriter = Attachment.BlobStoreWriterForBody(attachmentStream , database); string sha1DigestKey = blobStoreWriter.SHA1DigestString(); NUnit.Framework.Assert.IsTrue(sha1DigestKey.Contains("LmsoqJJ6LOn4YS60pYnvrKbBd64=" )); }
public void TestReopen() { var item = Encoding.UTF8.GetBytes("this is an item"); var key = new BlobKey(); _store.StoreBlob(item, key); var store2 = new BlobStore(_storePath, _store.EncryptionKey); var readItem = store2.BlobForKey(key); readItem.Should().Equal(item, "because the contents of a key should be the same in the second store"); readItem = _store.BlobForKey(key); readItem.Should().Equal(item, "because the contents of a key should be the same in the first store"); Verify(key, item); }
/// <exception cref="System.Exception"></exception> public virtual void TestBasicOperation() { BlobStore attachments = database.GetAttachments(); InputStream attachmentStream = GetAsset("attachment.png"); byte[] bytes = IOUtils.ToByteArray(attachmentStream); BlobStoreWriter blobStoreWriter = new BlobStoreWriter(attachments); blobStoreWriter.AppendData(bytes); blobStoreWriter.Finish(); blobStoreWriter.Install(); string sha1DigestKey = blobStoreWriter.SHA1DigestString(); BlobKey keyFromSha1 = new BlobKey(sha1DigestKey); NUnit.Framework.Assert.IsTrue(attachments.GetSizeOfBlob(keyFromSha1) == bytes.Length ); }
public void Setup() { _storePath = Path.GetTempPath(); _storePath = Path.Combine(_storePath, "CBL_BlobStore"); if (Directory.Exists(_storePath)) { Directory.Delete(_storePath, true); } if (_encrypt) { Trace.WriteLine("----- Now enabling attachment encryption ----"); } _store = new BlobStore(_storePath, _encrypt ? new SymmetricKey() : null); var encryptionMarkerPath = Path.Combine(_storePath, BlobStore.EncryptionMarkerFilename); var markerExists = File.Exists(encryptionMarkerPath); markerExists.Should().Be(_encrypt); }
public BlobStoreWriter(BlobStore store) { this.store = store; try { sha1Digest = MessageDigest.GetInstance("SHA-1"); sha1Digest.Reset(); md5Digest = MessageDigest.GetInstance("MD5"); md5Digest.Reset(); } catch (NotSupportedException e) { throw Misc.CreateExceptionAndLog(Log.To.Database, e, Tag, "Could not get an instance of SHA-1 or MD5 for BlobStoreWriter."); } try { OpenTempFile(); } catch (FileNotFoundException e) { throw Misc.CreateExceptionAndLog(Log.To.Database, e, Tag, "Unable to open temporary file for BlobStoreWriter."); } }
public virtual void TestStreamAttachmentBlobStoreWriter() { BlobStore attachments = database.GetAttachments(); BlobStoreWriter blobWriter = new BlobStoreWriter(attachments); string testBlob = "foo"; blobWriter.AppendData(Sharpen.Runtime.GetBytesForString(new string(testBlob))); blobWriter.Finish(); string sha1Base64Digest = "sha1-C+7Hteo/D9vJXQ3UfzxbwnXaijM="; NUnit.Framework.Assert.AreEqual(blobWriter.SHA1DigestString(), sha1Base64Digest); NUnit.Framework.Assert.AreEqual(blobWriter.MD5DigestString(), "md5-rL0Y20zC+Fzt72VPzMSk2A==" ); // install it blobWriter.Install(); // look it up in blob store and make sure it's there BlobKey blobKey = new BlobKey(sha1Base64Digest); byte[] blob = attachments.BlobForKey(blobKey); NUnit.Framework.Assert.IsTrue(Arrays.Equals(Sharpen.Runtime.GetBytesForString(testBlob , Sharpen.Extensions.GetEncoding("UTF-8")), blob)); }
public BlobStoreWriter(BlobStore store) { this.store = store; try { sha1Digest = MessageDigest.GetInstance("SHA-1"); sha1Digest.Reset(); md5Digest = MessageDigest.GetInstance("MD5"); md5Digest.Reset(); } catch (NoSuchAlgorithmException e) { throw new InvalidOperationException("Could not get an instance of SHA-1 or MD5.", e); } try { OpenTempFile(); } catch (FileNotFoundException e) { throw new InvalidOperationException("Unable to open temporary file.", e); } }
public BlobStoreWriter(BlobStore store) { this.store = store; try { sha1Digest = MessageDigest.GetInstance("SHA-1"); sha1Digest.Reset(); md5Digest = MessageDigest.GetInstance("MD5"); md5Digest.Reset(); } catch (NoSuchAlgorithmException e) { throw new InvalidOperationException(e); } try { OpenTempFile(); } catch (FileNotFoundException e) { throw new InvalidOperationException(e); } }
public BlobStoreWriter(BlobStore store) { this.store = store; try { sha1Digest = MessageDigest.GetInstance("SHA-1"); sha1Digest.Reset(); md5Digest = MessageDigest.GetInstance("MD5"); md5Digest.Reset(); } catch (NoSuchAlgorithmException e) { throw new InvalidOperationException("Could not get an instance of SHA-1 or MD5." , e); } try { OpenTempFile(); } catch (FileNotFoundException e) { throw new InvalidOperationException("Unable to open temporary file.", e); } }
internal bool Open() { if (_isOpen) { return true; } Log.D(TAG, "Opening {0}", Name); // Instantiate storage: //string storageType = Manager.StorageType ?? "SQLite"; Storage = new SqliteCouchStore(); Storage.Delegate = this; Log.D(TAG, "Using {0} for db at {1}", Storage.GetType(), Path); if (!Storage.Open(Path, Manager)) { return false; } Storage.AutoCompact = AUTO_COMPACT; // First-time setup: if (PrivateUUID() == null) { Storage.SetInfo("privateUUID", Misc.CreateGUID()); Storage.SetInfo("publicUUID", Misc.CreateGUID()); } var savedMaxRevDepth = _maxRevTreeDepth != 0 ? _maxRevTreeDepth.ToString() : Storage.GetInfo("max_revs"); int maxRevTreeDepth = 0; if (savedMaxRevDepth != null && int.TryParse(savedMaxRevDepth, out maxRevTreeDepth)) { MaxRevTreeDepth = maxRevTreeDepth; } else { MaxRevTreeDepth = DEFAULT_MAX_REVS; } // Open attachment store: string attachmentsPath = AttachmentStorePath; try { Attachments = new BlobStore(attachmentsPath); } catch(Exception e) { Log.W(TAG, String.Format("Couldn't open attachment store at {0}", attachmentsPath), e); Storage.Close(); Storage = null; return false; } _isOpen = true; return true; }
public void TestAttachments() { const string testAttachmentName = "test_attachment"; var attachments = database.Attachments; Assert.AreEqual(0, attachments.Count()); Assert.AreEqual(0, attachments.AllKeys().Count()); var attach1 = Encoding.UTF8.GetBytes("This is the body of attach1"); var props = new Dictionary <string, object> { { "foo", 1 }, { "bar", false }, { "_attachments", CreateAttachmentsDict(attach1, testAttachmentName, "text/plain", false) } }; Status status = new Status(); RevisionInternal rev1 = database.PutRevision(new RevisionInternal(props), null, false, status); Assert.AreEqual(StatusCode.Created, status.GetCode()); var att = database.GetAttachmentForRevision(rev1, testAttachmentName, status); Assert.IsNotNull(att, "Couldn't get attachment: Status {0}", status.GetCode()); Assert.AreEqual(attach1, att.Content); Assert.AreEqual("text/plain", att.ContentType); Assert.AreEqual(AttachmentEncoding.None, att.Encoding); var itemDict = new Dictionary <string, object> { { "content_type", "text/plain" }, { "digest", "sha1-gOHUOBmIMoDCrMuGyaLWzf1hQTE=" }, { "length", 27 }, { "stub", true }, { "revpos", 1 } }; var attachmentDict = new Dictionary <string, object> { { testAttachmentName, itemDict } }; var gotRev1 = database.GetDocumentWithIDAndRev(rev1.GetDocId(), rev1.GetRevId(), DocumentContentOptions.None); AssertDictionariesAreEqual(attachmentDict, gotRev1.GetAttachments()); itemDict.Remove("stub"); itemDict["data"] = Convert.ToBase64String(attach1); gotRev1 = database.GetDocumentWithIDAndRev(rev1.GetDocId(), rev1.GetRevId(), DocumentContentOptions.IncludeAttachments); var expandedRev = gotRev1.CopyWithDocID(rev1.GetDocId(), rev1.GetRevId()); Assert.IsTrue(database.ExpandAttachments(expandedRev, 0, false, true, status)); AssertDictionariesAreEqual(attachmentDict, expandedRev.GetAttachments()); // Add a second revision that doesn't update the attachment: props = new Dictionary <string, object> { { "_id", rev1.GetDocId() }, { "foo", 2 }, { "bazz", false }, { "_attachments", CreateAttachmentsStub(testAttachmentName) } }; var rev2 = database.PutRevision(new RevisionInternal(props), rev1.GetRevId(), status); Assert.AreEqual(StatusCode.Created, status.GetCode()); // Add a third revision of the same document: var attach2 = Encoding.UTF8.GetBytes("<html>And this is attach2</html>"); props = new Dictionary <string, object> { { "_id", rev2.GetDocId() }, { "foo", 2 }, { "bazz", false }, { "_attachments", CreateAttachmentsDict(attach2, testAttachmentName, "text/html", false) } }; var rev3 = database.PutRevision(new RevisionInternal(props), rev2.GetRevId(), status); Assert.AreEqual(StatusCode.Created, status.GetCode()); // Check the second revision's attachment att = database.GetAttachmentForRevision(rev2, testAttachmentName, status); Assert.IsNotNull(att, "Couldn't get attachment: Status {0}", status.GetCode()); Assert.AreEqual(attach1, att.Content); Assert.AreEqual("text/plain", att.ContentType); Assert.AreEqual(AttachmentEncoding.None, att.Encoding); expandedRev = rev2.CopyWithDocID(rev2.GetDocId(), rev2.GetRevId()); Assert.IsTrue(database.ExpandAttachments(expandedRev, 2, false, true, status)); AssertDictionariesAreEqual(new Dictionary <string, object> { { testAttachmentName, new Dictionary <string, object> { { "stub", true }, { "revpos", 1 } } } }, expandedRev.GetAttachments()); // Check the 3rd revision's attachment: att = database.GetAttachmentForRevision(rev3, testAttachmentName, status); Assert.IsNotNull(att, "Couldn't get attachment: Status {0}", status.GetCode()); Assert.AreEqual(attach2, att.Content); Assert.AreEqual("text/html", att.ContentType); Assert.AreEqual(AttachmentEncoding.None, att.Encoding); expandedRev = rev3.CopyWithDocID(rev3.GetDocId(), rev3.GetRevId()); Assert.IsTrue(database.ExpandAttachments(expandedRev, 2, false, true, status)); attachmentDict = new Dictionary <string, object> { { testAttachmentName, new Dictionary <string, object> { { "content_type", "text/html" }, { "data", "PGh0bWw+QW5kIHRoaXMgaXMgYXR0YWNoMjwvaHRtbD4=" }, { "digest", "sha1-s14XRTXlwvzYfjo1t1u0rjB+ZUA=" }, { "length", 32 }, { "revpos", 3 } } } }; AssertDictionariesAreEqual(attachmentDict, expandedRev.GetAttachments()); // Examine the attachment store: Assert.AreEqual(2, attachments.Count()); Assert.AreEqual(new HashSet <BlobKey> { BlobStore.KeyForBlob(attach1), BlobStore.KeyForBlob(attach2) }, attachments.AllKeys()); database.Compact(); Assert.AreEqual(1, attachments.Count()); Assert.AreEqual(new HashSet <BlobKey> { BlobStore.KeyForBlob(attach2) }, attachments.AllKeys()); }
public bool Open() { if (open) { return true; } // Create the storage engine. database = SQLiteStorageEngineFactory.CreateStorageEngine(); // Try to open the storage engine and stop if we fail. if (database == null || !database.Open(path)) { string msg = "Unable to create a storage engine, fatal error"; Log.E(Database.Tag, msg); throw new InvalidOperationException(msg); } // Stuff we need to initialize every time the sqliteDb opens: if (!Initialize("PRAGMA foreign_keys = ON;")) { Log.E(Database.Tag, "Error turning on foreign keys"); return false; } // Check the user_version number we last stored in the sqliteDb: int dbVersion = database.GetVersion(); // Incompatible version changes increment the hundreds' place: if (dbVersion >= 100) { Log.W(Database.Tag, "Database: Database version (" + dbVersion + ") is newer than I know how to work with" ); database.Close(); return false; } if (dbVersion < 1) { // First-time initialization: // (Note: Declaring revs.sequence as AUTOINCREMENT means the values will always be // monotonically increasing, never reused. See <http://www.sqlite.org/autoinc.html>) if (!Initialize(Schema)) { database.Close(); return false; } dbVersion = 3; } if (dbVersion < 2) { // Version 2: added attachments.revpos string upgradeSql = "ALTER TABLE attachments ADD COLUMN revpos INTEGER DEFAULT 0; " + "PRAGMA user_version = 2"; if (!Initialize(upgradeSql)) { database.Close(); return false; } dbVersion = 2; } if (dbVersion < 3) { string upgradeSql = "CREATE TABLE localdocs ( " + "docid TEXT UNIQUE NOT NULL, " + "revid TEXT NOT NULL, " + "json BLOB); " + "CREATE INDEX localdocs_by_docid ON localdocs(docid); " + "PRAGMA user_version = 3"; if (!Initialize(upgradeSql)) { database.Close(); return false; } dbVersion = 3; } if (dbVersion < 4) { string upgradeSql = "CREATE TABLE info ( " + "key TEXT PRIMARY KEY, " + "value TEXT); " + "INSERT INTO INFO (key, value) VALUES ('privateUUID', '" + Misc.TDCreateUUID( ) + "'); " + "INSERT INTO INFO (key, value) VALUES ('publicUUID', '" + Misc.TDCreateUUID () + "'); " + "PRAGMA user_version = 4"; if (!Initialize(upgradeSql)) { database.Close(); return false; } } try { attachments = new BlobStore(GetAttachmentStorePath()); } catch (ArgumentException e) { Log.E(Database.Tag, "Could not initialize attachment store", e); database.Close(); return false; } open = true; return true; }
public RevisionInternal UpdateAttachment(string filename, InputStream contentStream , string contentType, string docID, string oldRevID) { bool isSuccessful = false; if (filename == null || filename.Length == 0 || (contentStream != null && contentType == null) || (oldRevID != null && docID == null) || (contentStream != null && docID == null)) { throw new CouchbaseLiteException(Status.BadRequest); } BeginTransaction(); try { RevisionInternal oldRev = new RevisionInternal(docID, oldRevID, false, this); if (oldRevID != null) { // Load existing revision if this is a replacement: try { LoadRevisionBody(oldRev, EnumSet.NoneOf<Database.TDContentOptions>()); } catch (CouchbaseLiteException e) { if (e.GetCBLStatus().GetCode() == Status.NotFound && ExistsDocumentWithIDAndRev(docID , null)) { throw new CouchbaseLiteException(Status.Conflict); } } IDictionary<string, object> oldRevProps = oldRev.GetProperties(); IDictionary<string, object> attachments = null; if (oldRevProps != null) { attachments = (IDictionary<string, object>)oldRevProps.Get("_attachments"); } if (contentStream == null && attachments != null && !attachments.ContainsKey(filename )) { throw new CouchbaseLiteException(Status.NotFound); } // Remove the _attachments stubs so putRevision: doesn't copy the rows for me // OPT: Would be better if I could tell loadRevisionBody: not to add it if (attachments != null) { IDictionary<string, object> properties = new Dictionary<string, object>(oldRev.GetProperties ()); Sharpen.Collections.Remove(properties, "_attachments"); oldRev.SetBody(new Body(properties)); } } else { // If this creates a new doc, it needs a body: oldRev.SetBody(new Body(new Dictionary<string, object>())); } // Create a new revision: Status putStatus = new Status(); RevisionInternal newRev = PutRevision(oldRev, oldRevID, false, putStatus); if (newRev == null) { return null; } if (oldRevID != null) { // Copy all attachment rows _except_ for the one being updated: string[] args = new string[] { System.Convert.ToString(newRev.GetSequence()), System.Convert.ToString (oldRev.GetSequence()), filename }; database.ExecSQL("INSERT INTO attachments " + "(sequence, filename, key, type, length, revpos) " + "SELECT ?, filename, key, type, length, revpos FROM attachments " + "WHERE sequence=? AND filename != ?" , args); } if (contentStream != null) { // If not deleting, add a new attachment entry: InsertAttachmentForSequenceWithNameAndType(contentStream, newRev.GetSequence(), filename , contentType, newRev.GetGeneration()); } isSuccessful = true; return newRev; } catch (SQLException e) { Log.E(Tag, "Error updating attachment", e); throw new CouchbaseLiteException(new Status(Status.InternalServerError)); } finally { EndTransaction(isSuccessful); } }
public void StubOutAttachmentsIn(RevisionInternal rev, int minRevPos) { if (minRevPos <= 1) { return; } IDictionary<string, object> properties = (IDictionary<string, object>)rev.GetProperties (); IDictionary<string, object> attachments = null; if (properties != null) { attachments = (IDictionary<string, object>)properties.Get("_attachments"); } IDictionary<string, object> editedProperties = null; IDictionary<string, object> editedAttachments = null; foreach (string name in attachments.Keys) { IDictionary<string, object> attachment = (IDictionary<string, object>)attachments .Get(name); int revPos = (int)attachment.Get("revpos"); object stub = attachment.Get("stub"); if (revPos > 0 && revPos < minRevPos && (stub == null)) { // Strip this attachment's body. First make its dictionary mutable: if (editedProperties == null) { editedProperties = new Dictionary<string, object>(properties); editedAttachments = new Dictionary<string, object>(attachments); editedProperties.Put("_attachments", editedAttachments); } // ...then remove the 'data' and 'follows' key: IDictionary<string, object> editedAttachment = new Dictionary<string, object>(attachment ); Sharpen.Collections.Remove(editedAttachment, "data"); Sharpen.Collections.Remove(editedAttachment, "follows"); editedAttachment.Put("stub", true); editedAttachments.Put(name, editedAttachment); Log.D(Database.Tag, "Stubbed out attachment" + rev + " " + name + ": revpos" + revPos + " " + minRevPos); } } if (editedProperties != null) { rev.SetProperties(editedProperties); } }
internal void OpenWithOptions(DatabaseOptions options) { if(IsOpen) { return; } Log.To.Database.I(TAG, "Opening {0}", this); _readonly = _readonly || options.ReadOnly; // Instantiate storage: string storageType = options.StorageType ?? Manager.StorageType ?? StorageEngineTypes.SQLite; var primaryStorage = GetStorageClass(storageType); if(primaryStorage == null) { if(storageType == StorageEngineTypes.SQLite) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.InvalidStorageType, TAG, "No implementation found for SQLite storage. For more information, see " + "https://github.com/couchbase/couchbase-lite-net/wiki/Error-Dictionary#cblcs0001"); } else { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.InvalidStorageType, TAG, "No implementation found for ForestDB storage. For more information, see " + "https://github.com/couchbase/couchbase-lite-net/wiki/Error-Dictionary#cblcs0002"); } } var upgrade = false; var primarySQLite = storageType == StorageEngineTypes.SQLite; var otherStorage = primarySQLite ? GetStorageClass(StorageEngineTypes.ForestDB) : GetStorageClass(StorageEngineTypes.SQLite); var primaryStorageInstance = (ICouchStore)Activator.CreateInstance(primaryStorage); var otherStorageInstance = otherStorage != null ? (ICouchStore)Activator.CreateInstance(otherStorage) : null; if(options.StorageType != null) { // If explicit storage type given in options, always use primary storage type, // and if secondary db exists, try to upgrade from it: upgrade = otherStorageInstance != null && otherStorageInstance.DatabaseExistsIn(DbDirectory) && !primaryStorageInstance.DatabaseExistsIn(DbDirectory); if(upgrade && primarySQLite) { throw Misc.CreateExceptionAndLog(Log.To.Upgrade, StatusCode.InvalidStorageType, TAG, "Upgrades from ForestDB to SQLite are not supported. For more information see " + "https://github.com/couchbase/couchbase-lite-net/wiki/Error-Dictionary#cbldb0001"); } } else { // If options don't specify, use primary unless secondary db already exists in dir: if(otherStorageInstance != null && otherStorageInstance.DatabaseExistsIn(DbDirectory)) { primaryStorageInstance = otherStorageInstance; } } Log.To.Database.I(TAG, "Using {0} for db at {1}; upgrade={2}", primaryStorage.FullName, DbDirectory, upgrade); Storage = primaryStorageInstance; Storage.Delegate = this; Storage.AutoCompact = AUTO_COMPACT; // Encryption: var encryptionKey = options.EncryptionKey; if(encryptionKey != null) { Storage.SetEncryptionKey(encryptionKey); } // Open the storage! try { Storage.Open(DbDirectory, Manager, _readonly); } catch(CouchbaseLiteException) { Storage.Close(); Log.To.Database.E(TAG, "Failed to open storage for database, rethrowing..."); throw; } catch(Exception e) { Storage.Close(); throw Misc.CreateExceptionAndLog(Log.To.Database, e, TAG, "Got exception while opening storage for database"); } // First-time setup: if(PrivateUUID() == null) { Storage.SetInfo("privateUUID", Misc.CreateGUID()); Storage.SetInfo("publicUUID", Misc.CreateGUID()); } var savedMaxRevDepth = _maxRevTreeDepth != 0 ? _maxRevTreeDepth.ToString() : Storage.GetInfo("max_revs"); int maxRevTreeDepth = 0; if(savedMaxRevDepth != null && int.TryParse(savedMaxRevDepth, out maxRevTreeDepth)) { SetMaxRevTreeDepth(maxRevTreeDepth); } else { SetMaxRevTreeDepth(Manager.DefaultMaxRevTreeDepth); } // Open attachment store: string attachmentsPath = AttachmentStorePath; try { Attachments = new BlobStore(attachmentsPath, encryptionKey); } catch(CouchbaseLiteException) { Log.To.Database.E(TAG, "Error creating blob store at {0}, rethrowing...", attachmentsPath); Storage.Close(); Storage = null; throw; } catch(Exception e) { Storage.Close(); Storage = null; throw Misc.CreateExceptionAndLog(Log.To.Database, e, TAG, "Got exception creating blob store at {0}", attachmentsPath); } IsOpen = true; if(upgrade) { var upgrader = primarySQLite ? Storage.CreateUpgrader(this, DbDirectory) : otherStorageInstance.CreateUpgrader(this, DbDirectory); try { upgrader.Import(); } catch(CouchbaseLiteException e) { Log.To.Database.E(TAG, "Upgrade failed for {0} (Status {1}), rethrowing...", DbDirectory, e.CBLStatus); upgrader.Backout(); Close(); throw; } } _expirePurgeTimer = new Timer(PurgeExpired, null, HousekeepingDelayAfterOpen, TimeSpan.FromMilliseconds(-1)); }
/// <exception cref="System.Exception"></exception> public virtual void TestPutLargeAttachment() { string testAttachmentName = "test_attachment"; BlobStore attachments = database.GetAttachments(); attachments.DeleteBlobs(); NUnit.Framework.Assert.AreEqual(0, attachments.Count()); Status status = new Status(); IDictionary <string, object> rev1Properties = new Dictionary <string, object>(); rev1Properties.Put("foo", 1); rev1Properties.Put("bar", false); RevisionInternal rev1 = database.PutRevision(new RevisionInternal(rev1Properties, database), null, false, status); NUnit.Framework.Assert.AreEqual(Status.Created, status.GetCode()); StringBuilder largeAttachment = new StringBuilder(); for (int i = 0; i < Database.kBigAttachmentLength; i++) { largeAttachment.Append("big attachment!"); } byte[] attach1 = Sharpen.Runtime.GetBytesForString(largeAttachment.ToString()); database.InsertAttachmentForSequenceWithNameAndType(new ByteArrayInputStream(attach1 ), rev1.GetSequence(), testAttachmentName, "text/plain", rev1.GetGeneration()); Attachment attachment = database.GetAttachmentForSequence(rev1.GetSequence(), testAttachmentName ); NUnit.Framework.Assert.AreEqual("text/plain", attachment.GetContentType()); byte[] data = IOUtils.ToByteArray(attachment.GetContent()); NUnit.Framework.Assert.IsTrue(Arrays.Equals(attach1, data)); EnumSet <Database.TDContentOptions> contentOptions = EnumSet.Of(Database.TDContentOptions .TDIncludeAttachments, Database.TDContentOptions.TDBigAttachmentsFollow); IDictionary <string, object> attachmentDictForSequence = database.GetAttachmentsDictForSequenceWithContent (rev1.GetSequence(), contentOptions); IDictionary <string, object> innerDict = (IDictionary <string, object>)attachmentDictForSequence .Get(testAttachmentName); if (!innerDict.ContainsKey("stub")) { throw new RuntimeException("Expected attachment dict to have 'stub' key"); } if (((bool)innerDict.Get("stub")) == false) { throw new RuntimeException("Expected attachment dict 'stub' key to be true"); } if (!innerDict.ContainsKey("follows")) { throw new RuntimeException("Expected attachment dict to have 'follows' key"); } RevisionInternal rev1WithAttachments = database.GetDocumentWithIDAndRev(rev1.GetDocId (), rev1.GetRevId(), contentOptions); // Map<String,Object> rev1PropertiesPrime = rev1WithAttachments.getProperties(); // rev1PropertiesPrime.put("foo", 2); IDictionary <string, object> rev1WithAttachmentsProperties = rev1WithAttachments.GetProperties (); IDictionary <string, object> rev2Properties = new Dictionary <string, object>(); rev2Properties.Put("_id", rev1WithAttachmentsProperties.Get("_id")); rev2Properties.Put("foo", 2); RevisionInternal newRev = new RevisionInternal(rev2Properties, database); RevisionInternal rev2 = database.PutRevision(newRev, rev1WithAttachments.GetRevId (), false, status); NUnit.Framework.Assert.AreEqual(Status.Created, status.GetCode()); database.CopyAttachmentNamedFromSequenceToSequence(testAttachmentName, rev1WithAttachments .GetSequence(), rev2.GetSequence()); // Check the 2nd revision's attachment: Attachment rev2FetchedAttachment = database.GetAttachmentForSequence(rev2.GetSequence (), testAttachmentName); NUnit.Framework.Assert.AreEqual(attachment.GetLength(), rev2FetchedAttachment.GetLength ()); NUnit.Framework.Assert.AreEqual(attachment.GetMetadata(), rev2FetchedAttachment.GetMetadata ()); NUnit.Framework.Assert.AreEqual(attachment.GetContentType(), rev2FetchedAttachment .GetContentType()); // Add a third revision of the same document: IDictionary <string, object> rev3Properties = new Dictionary <string, object>(); rev3Properties.Put("_id", rev2.GetProperties().Get("_id")); rev3Properties.Put("foo", 3); rev3Properties.Put("baz", false); RevisionInternal rev3 = new RevisionInternal(rev3Properties, database); rev3 = database.PutRevision(rev3, rev2.GetRevId(), false, status); NUnit.Framework.Assert.AreEqual(Status.Created, status.GetCode()); byte[] attach3 = Sharpen.Runtime.GetBytesForString("<html><blink>attach3</blink></html>" ); database.InsertAttachmentForSequenceWithNameAndType(new ByteArrayInputStream(attach3 ), rev3.GetSequence(), testAttachmentName, "text/html", rev3.GetGeneration()); // Check the 3rd revision's attachment: Attachment rev3FetchedAttachment = database.GetAttachmentForSequence(rev3.GetSequence (), testAttachmentName); data = IOUtils.ToByteArray(rev3FetchedAttachment.GetContent()); NUnit.Framework.Assert.IsTrue(Arrays.Equals(attach3, data)); NUnit.Framework.Assert.AreEqual("text/html", rev3FetchedAttachment.GetContentType ()); // TODO: why doesn't this work? // Assert.assertEquals(attach3.length, rev3FetchedAttachment.getLength()); ICollection <BlobKey> blobKeys = database.GetAttachments().AllKeys(); NUnit.Framework.Assert.AreEqual(2, blobKeys.Count); database.Compact(); blobKeys = database.GetAttachments().AllKeys(); NUnit.Framework.Assert.AreEqual(1, blobKeys.Count); }
public void TearDown() { _store = null; Directory.Delete(_storePath, true); }
/// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> public virtual void TestPutAttachment() { string testAttachmentName = "test_attachment"; BlobStore attachments = database.GetAttachments(); attachments.DeleteBlobs(); NUnit.Framework.Assert.AreEqual(0, attachments.Count()); // Put a revision that includes an _attachments dict: byte[] attach1 = Sharpen.Runtime.GetBytesForString("This is the body of attach1"); string base64 = Base64.EncodeBytes(attach1); IDictionary <string, object> attachment = new Dictionary <string, object>(); attachment.Put("content_type", "text/plain"); attachment.Put("data", base64); IDictionary <string, object> attachmentDict = new Dictionary <string, object>(); attachmentDict.Put(testAttachmentName, attachment); IDictionary <string, object> properties = new Dictionary <string, object>(); properties.Put("foo", 1); properties.Put("bar", false); properties.Put("_attachments", attachmentDict); RevisionInternal rev1 = database.PutRevision(new RevisionInternal(properties, database ), null, false); // Examine the attachment store: NUnit.Framework.Assert.AreEqual(1, attachments.Count()); // Get the revision: RevisionInternal gotRev1 = database.GetDocumentWithIDAndRev(rev1.GetDocId(), rev1 .GetRevId(), EnumSet.NoneOf <Database.TDContentOptions>()); IDictionary <string, object> gotAttachmentDict = (IDictionary <string, object>)gotRev1 .GetProperties().Get("_attachments"); IDictionary <string, object> innerDict = new Dictionary <string, object>(); innerDict.Put("content_type", "text/plain"); innerDict.Put("digest", "sha1-gOHUOBmIMoDCrMuGyaLWzf1hQTE="); innerDict.Put("length", 27); innerDict.Put("stub", true); innerDict.Put("revpos", 1); IDictionary <string, object> expectAttachmentDict = new Dictionary <string, object> (); expectAttachmentDict.Put(testAttachmentName, innerDict); NUnit.Framework.Assert.AreEqual(expectAttachmentDict, gotAttachmentDict); // Update the attachment directly: byte[] attachv2 = Sharpen.Runtime.GetBytesForString("Replaced body of attach"); bool gotExpectedErrorCode = false; try { database.UpdateAttachment(testAttachmentName, new ByteArrayInputStream(attachv2), "application/foo", rev1.GetDocId(), null); } catch (CouchbaseLiteException e) { gotExpectedErrorCode = (e.GetCBLStatus().GetCode() == Status.Conflict); } NUnit.Framework.Assert.IsTrue(gotExpectedErrorCode); gotExpectedErrorCode = false; try { database.UpdateAttachment(testAttachmentName, new ByteArrayInputStream(attachv2), "application/foo", rev1.GetDocId(), "1-bogus"); } catch (CouchbaseLiteException e) { gotExpectedErrorCode = (e.GetCBLStatus().GetCode() == Status.Conflict); } NUnit.Framework.Assert.IsTrue(gotExpectedErrorCode); gotExpectedErrorCode = false; RevisionInternal rev2 = null; try { rev2 = database.UpdateAttachment(testAttachmentName, new ByteArrayInputStream(attachv2 ), "application/foo", rev1.GetDocId(), rev1.GetRevId()); } catch (CouchbaseLiteException) { gotExpectedErrorCode = true; } NUnit.Framework.Assert.IsFalse(gotExpectedErrorCode); NUnit.Framework.Assert.AreEqual(rev1.GetDocId(), rev2.GetDocId()); NUnit.Framework.Assert.AreEqual(2, rev2.GetGeneration()); // Get the updated revision: RevisionInternal gotRev2 = database.GetDocumentWithIDAndRev(rev2.GetDocId(), rev2 .GetRevId(), EnumSet.NoneOf <Database.TDContentOptions>()); attachmentDict = (IDictionary <string, object>)gotRev2.GetProperties().Get("_attachments" ); innerDict = new Dictionary <string, object>(); innerDict.Put("content_type", "application/foo"); innerDict.Put("digest", "sha1-mbT3208HI3PZgbG4zYWbDW2HsPk="); innerDict.Put("length", 23); innerDict.Put("stub", true); innerDict.Put("revpos", 2); expectAttachmentDict.Put(testAttachmentName, innerDict); NUnit.Framework.Assert.AreEqual(expectAttachmentDict, attachmentDict); // Delete the attachment: gotExpectedErrorCode = false; try { database.UpdateAttachment("nosuchattach", null, null, rev2.GetDocId(), rev2.GetRevId ()); } catch (CouchbaseLiteException e) { gotExpectedErrorCode = (e.GetCBLStatus().GetCode() == Status.NotFound); } NUnit.Framework.Assert.IsTrue(gotExpectedErrorCode); gotExpectedErrorCode = false; try { database.UpdateAttachment("nosuchattach", null, null, "nosuchdoc", "nosuchrev"); } catch (CouchbaseLiteException e) { gotExpectedErrorCode = (e.GetCBLStatus().GetCode() == Status.NotFound); } NUnit.Framework.Assert.IsTrue(gotExpectedErrorCode); RevisionInternal rev3 = database.UpdateAttachment(testAttachmentName, null, null, rev2.GetDocId(), rev2.GetRevId()); NUnit.Framework.Assert.AreEqual(rev2.GetDocId(), rev3.GetDocId()); NUnit.Framework.Assert.AreEqual(3, rev3.GetGeneration()); // Get the updated revision: RevisionInternal gotRev3 = database.GetDocumentWithIDAndRev(rev3.GetDocId(), rev3 .GetRevId(), EnumSet.NoneOf <Database.TDContentOptions>()); attachmentDict = (IDictionary <string, object>)gotRev3.GetProperties().Get("_attachments" ); NUnit.Framework.Assert.IsNull(attachmentDict); database.Close(); }
public AtomicAction ActionToChangeEncryptionKey(SymmetricKey newKey) { var action = new AtomicAction(); // Find all the blob files: var blobs = default(string[]); var oldKey = EncryptionKey; blobs = Directory.GetFiles(_path, "*" + FileExtension); if (blobs.Length == 0) { // No blobs, so nothing to encrypt. Just add/remove the encryption marker file: action.AddLogic(() => { Log.To.NoDomain.D(TAG, "{0} {1}", (newKey != null) ? "encrypting" : "decrypting", _path); Log.To.NoDomain.D(TAG, " No blobs to copy; done."); EncryptionKey = newKey; MarkEncrypted(newKey != null); }, () => { EncryptionKey = oldKey; MarkEncrypted(oldKey != null); }, null); return action; } // Create a new directory for the new blob store. Have to do this now, before starting the // action, because farther down we create an action to move it... var tempPath = Path.Combine(Path.GetTempPath(), String.Format("CouchbaseLite-Temp-{0}", Misc.CreateGUID())); action.AddLogic(() => { Log.To.NoDomain.D(TAG, "{0} {1}", (newKey != null) ? "encrypting" : "decrypting", _path); Directory.CreateDirectory(tempPath); }, () => Directory.Delete(tempPath, true), null); var tempStore = default(BlobStore); action.AddLogic(() => { tempStore = new BlobStore(tempPath, newKey); tempStore.MarkEncrypted(true); }, null, null); // Copy each of my blobs into the new store (which will update its encryption): action.AddLogic(() => { foreach(var blobName in blobs) { // Copy file by reading with old key and writing with new one: Log.To.NoDomain.D(TAG, " Copying {0}", blobName); Stream readStream = File.Open(blobName, FileMode.Open, FileAccess.Read, FileShare.Read); if(EncryptionKey != null) { readStream = EncryptionKey.DecryptStream(readStream); } var writer = new BlobStoreWriter(tempStore); try { writer.Read(readStream); writer.Finish(); writer.Install(); } catch(Exception) { writer.Cancel(); throw; } finally { readStream.Dispose(); } } }, null, null); // Replace the attachment dir with the new one: action.AddLogic(AtomicAction.MoveDirectory(tempPath, _path)); // Finally update EncryptionKey: action.AddLogic(() => { EncryptionKey = newKey; }, () => { EncryptionKey = oldKey; }, null); return action; }
/// <exception cref="System.Exception"></exception> public virtual void TestAttachments() { string testAttachmentName = "test_attachment"; BlobStore attachments = database.GetAttachments(); NUnit.Framework.Assert.AreEqual(0, attachments.Count()); NUnit.Framework.Assert.AreEqual(new HashSet <object>(), attachments.AllKeys()); Status status = new Status(); IDictionary <string, object> rev1Properties = new Dictionary <string, object>(); rev1Properties.Put("foo", 1); rev1Properties.Put("bar", false); RevisionInternal rev1 = database.PutRevision(new RevisionInternal(rev1Properties, database), null, false, status); NUnit.Framework.Assert.AreEqual(Status.Created, status.GetCode()); byte[] attach1 = Sharpen.Runtime.GetBytesForString("This is the body of attach1"); database.InsertAttachmentForSequenceWithNameAndType(new ByteArrayInputStream(attach1 ), rev1.GetSequence(), testAttachmentName, "text/plain", rev1.GetGeneration()); NUnit.Framework.Assert.AreEqual(Status.Created, status.GetCode()); Attachment attachment = database.GetAttachmentForSequence(rev1.GetSequence(), testAttachmentName ); NUnit.Framework.Assert.AreEqual("text/plain", attachment.GetContentType()); byte[] data = IOUtils.ToByteArray(attachment.GetContent()); NUnit.Framework.Assert.IsTrue(Arrays.Equals(attach1, data)); IDictionary <string, object> innerDict = new Dictionary <string, object>(); innerDict.Put("content_type", "text/plain"); innerDict.Put("digest", "sha1-gOHUOBmIMoDCrMuGyaLWzf1hQTE="); innerDict.Put("length", 27); innerDict.Put("stub", true); innerDict.Put("revpos", 1); IDictionary <string, object> attachmentDict = new Dictionary <string, object>(); attachmentDict.Put(testAttachmentName, innerDict); IDictionary <string, object> attachmentDictForSequence = database.GetAttachmentsDictForSequenceWithContent (rev1.GetSequence(), EnumSet.NoneOf <Database.TDContentOptions>()); NUnit.Framework.Assert.AreEqual(attachmentDict, attachmentDictForSequence); RevisionInternal gotRev1 = database.GetDocumentWithIDAndRev(rev1.GetDocId(), rev1 .GetRevId(), EnumSet.NoneOf <Database.TDContentOptions>()); IDictionary <string, object> gotAttachmentDict = (IDictionary <string, object>)gotRev1 .GetProperties().Get("_attachments"); NUnit.Framework.Assert.AreEqual(attachmentDict, gotAttachmentDict); // Check the attachment dict, with attachments included: Sharpen.Collections.Remove(innerDict, "stub"); innerDict.Put("data", Base64.EncodeBytes(attach1)); attachmentDictForSequence = database.GetAttachmentsDictForSequenceWithContent(rev1 .GetSequence(), EnumSet.Of(Database.TDContentOptions.TDIncludeAttachments)); NUnit.Framework.Assert.AreEqual(attachmentDict, attachmentDictForSequence); gotRev1 = database.GetDocumentWithIDAndRev(rev1.GetDocId(), rev1.GetRevId(), EnumSet .Of(Database.TDContentOptions.TDIncludeAttachments)); gotAttachmentDict = (IDictionary <string, object>)gotRev1.GetProperties().Get("_attachments" ); NUnit.Framework.Assert.AreEqual(attachmentDict, gotAttachmentDict); // Add a second revision that doesn't update the attachment: IDictionary <string, object> rev2Properties = new Dictionary <string, object>(); rev2Properties.Put("_id", rev1.GetDocId()); rev2Properties.Put("foo", 2); rev2Properties.Put("bazz", false); RevisionInternal rev2 = database.PutRevision(new RevisionInternal(rev2Properties, database), rev1.GetRevId(), false, status); NUnit.Framework.Assert.AreEqual(Status.Created, status.GetCode()); database.CopyAttachmentNamedFromSequenceToSequence(testAttachmentName, rev1.GetSequence (), rev2.GetSequence()); // Add a third revision of the same document: IDictionary <string, object> rev3Properties = new Dictionary <string, object>(); rev3Properties.Put("_id", rev2.GetDocId()); rev3Properties.Put("foo", 2); rev3Properties.Put("bazz", false); RevisionInternal rev3 = database.PutRevision(new RevisionInternal(rev3Properties, database), rev2.GetRevId(), false, status); NUnit.Framework.Assert.AreEqual(Status.Created, status.GetCode()); byte[] attach2 = Sharpen.Runtime.GetBytesForString("<html>And this is attach2</html>" ); database.InsertAttachmentForSequenceWithNameAndType(new ByteArrayInputStream(attach2 ), rev3.GetSequence(), testAttachmentName, "text/html", rev2.GetGeneration()); // Check the 2nd revision's attachment: Attachment attachment2 = database.GetAttachmentForSequence(rev2.GetSequence(), testAttachmentName ); NUnit.Framework.Assert.AreEqual("text/plain", attachment2.GetContentType()); data = IOUtils.ToByteArray(attachment2.GetContent()); NUnit.Framework.Assert.IsTrue(Arrays.Equals(attach1, data)); // Check the 3rd revision's attachment: Attachment attachment3 = database.GetAttachmentForSequence(rev3.GetSequence(), testAttachmentName ); NUnit.Framework.Assert.AreEqual("text/html", attachment3.GetContentType()); data = IOUtils.ToByteArray(attachment3.GetContent()); NUnit.Framework.Assert.IsTrue(Arrays.Equals(attach2, data)); // Examine the attachment store: NUnit.Framework.Assert.AreEqual(2, attachments.Count()); ICollection <BlobKey> expected = new HashSet <BlobKey>(); expected.AddItem(BlobStore.KeyForBlob(attach1)); expected.AddItem(BlobStore.KeyForBlob(attach2)); NUnit.Framework.Assert.AreEqual(expected, attachments.AllKeys()); database.Compact(); // This clears the body of the first revision NUnit.Framework.Assert.AreEqual(1, attachments.Count()); ICollection <BlobKey> expected2 = new HashSet <BlobKey>(); expected2.AddItem(BlobStore.KeyForBlob(attach2)); NUnit.Framework.Assert.AreEqual(expected2, attachments.AllKeys()); }
public void TestAttachments() { var testAttachmentName = "test_attachment"; var attachments = database.Attachments; Assert.AreEqual(0, attachments.Count()); Assert.AreEqual(0, attachments.AllKeys().Count()); var rev1Properties = new Dictionary <string, object>(); rev1Properties["foo"] = 1; rev1Properties["bar"] = false; var status = new Status(); var rev1 = database.PutRevision( new RevisionInternal(rev1Properties), null, false, status); Assert.AreEqual(StatusCode.Created, status.GetCode()); var attach1 = Encoding.UTF8.GetBytes("This is the body of attach1"); database.InsertAttachmentForSequenceWithNameAndType( new ByteArrayInputStream(attach1), rev1.GetSequence(), testAttachmentName, "text/plain", rev1.GetGeneration()); //We must set the no_attachments column for the rev to false, as we are using an internal //private API call above (database.insertAttachmentForSequenceWithNameAndType) which does //not set the no_attachments column on revs table try { var args = new ContentValues(); args.Put("no_attachments", false); database.StorageEngine.Update( "revs", args, "sequence=?", new[] { rev1.GetSequence().ToString() } ); } catch (SQLException e) { Log.E(Tag, "Error setting rev1 no_attachments to false", e); throw new CouchbaseLiteException(StatusCode.InternalServerError); } var attachment = database.GetAttachmentForSequence( rev1.GetSequence(), testAttachmentName ); Assert.AreEqual("text/plain", attachment.ContentType); var data = attachment.Content.ToArray(); Assert.IsTrue(Arrays.Equals(attach1, data)); attachment.Dispose(); var innerDict = new Dictionary <string, object>(); innerDict["content_type"] = "text/plain"; innerDict["digest"] = "sha1-gOHUOBmIMoDCrMuGyaLWzf1hQTE="; innerDict["length"] = 27; innerDict["stub"] = true; innerDict["revpos"] = 1; var attachmentDict = new Dictionary <string, object>(); attachmentDict[testAttachmentName] = innerDict; var attachmentDictForSequence = database.GetAttachmentsDictForSequenceWithContent(rev1.GetSequence(), DocumentContentOptions.None); Assert.AreEqual(new SortedDictionary <string, object>(attachmentDict), new SortedDictionary <string, object>(attachmentDictForSequence));//Assert.AreEqual(1, attachmentDictForSequence.Count); var gotRev1 = database.GetDocumentWithIDAndRev(rev1.GetDocId(), rev1.GetRevId(), DocumentContentOptions.IncludeAttachments); var gotAttachmentDict = gotRev1.GetProperties() .Get("_attachments") .AsDictionary <string, object>(); Assert.AreEqual(attachmentDict.Select(kvp => kvp.Key).OrderBy(k => k), gotAttachmentDict.Select(kvp => kvp.Key).OrderBy(k => k)); // Check the attachment dict, with attachments included: innerDict.Remove("stub"); innerDict.Put("data", Convert.ToBase64String(attach1)); attachmentDictForSequence = database.GetAttachmentsDictForSequenceWithContent( rev1.GetSequence(), DocumentContentOptions.IncludeAttachments); Assert.AreEqual(new SortedDictionary <string, object>(attachmentDict[testAttachmentName].AsDictionary <string, object>()), new SortedDictionary <string, object>(attachmentDictForSequence[testAttachmentName].AsDictionary <string, object>())); gotRev1 = database.GetDocumentWithIDAndRev( rev1.GetDocId(), rev1.GetRevId(), DocumentContentOptions.IncludeAttachments); gotAttachmentDict = gotRev1.GetProperties() .Get("_attachments") .AsDictionary <string, object>() .Get(testAttachmentName) .AsDictionary <string, object>(); Assert.AreEqual(innerDict.Select(kvp => kvp.Key).OrderBy(k => k), gotAttachmentDict.Select(kvp => kvp.Key).OrderBy(k => k)); // Add a second revision that doesn't update the attachment: database.BeginTransaction(); var rev2Properties = new Dictionary <string, object>(); rev2Properties.Put("_id", rev1.GetDocId()); rev2Properties["foo"] = 2; rev2Properties["bazz"] = false; var rev2 = database.PutRevision(new RevisionInternal(rev2Properties), rev1.GetRevId(), false, status); Assert.AreEqual(StatusCode.Created, status.GetCode()); database.CopyAttachmentNamedFromSequenceToSequence( testAttachmentName, rev1.GetSequence(), rev2.GetSequence()); database.EndTransaction(true); // Add a third revision of the same document: var rev3Properties = new Dictionary <string, object>(); rev3Properties.Put("_id", rev2.GetDocId()); rev3Properties["foo"] = 2; rev3Properties["bazz"] = false; database.BeginTransaction(); var rev3 = database.PutRevision(new RevisionInternal( rev3Properties), rev2.GetRevId(), false, status); Assert.AreEqual(StatusCode.Created, status.GetCode()); var attach2 = Encoding.UTF8.GetBytes("<html>And this is attach2</html>"); database.InsertAttachmentForSequenceWithNameAndType( new ByteArrayInputStream(attach2), rev3.GetSequence(), testAttachmentName, "text/html", rev2.GetGeneration()); database.EndTransaction(true); // Check the 2nd revision's attachment: var attachment2 = database.GetAttachmentForSequence(rev2.GetSequence(), testAttachmentName); Assert.AreEqual("text/plain", attachment2.ContentType); data = attachment2.Content.ToArray(); Assert.IsTrue(Arrays.Equals(attach1, data)); attachment2.Dispose(); // Check the 3rd revision's attachment: var attachment3 = database.GetAttachmentForSequence(rev3.GetSequence(), testAttachmentName); Assert.AreEqual("text/html", attachment3.ContentType); data = attachment3.Content.ToArray(); Assert.IsTrue(Arrays.Equals(attach2, data)); var attachmentDictForRev3 = database.GetAttachmentsDictForSequenceWithContent(rev3.GetSequence(), DocumentContentOptions.None) .Get(testAttachmentName) .AsDictionary <string, object>(); if (attachmentDictForRev3.ContainsKey("follows")) { if (((bool)attachmentDictForRev3.Get("follows")) == true) { throw new RuntimeException("Did not expected attachment dict 'follows' key to be true" ); } else { throw new RuntimeException("Did not expected attachment dict to have 'follows' key" ); } } attachment3.Dispose(); // Examine the attachment store: Assert.AreEqual(2, attachments.Count()); var expected = new HashSet <BlobKey>(); expected.AddItem(BlobStore.KeyForBlob(attach1)); expected.AddItem(BlobStore.KeyForBlob(attach2)); Assert.AreEqual(expected.Count, attachments.AllKeys().Count()); foreach (var key in attachments.AllKeys()) { Assert.IsTrue(expected.Contains(key)); } database.Compact(); // This clears the body of the first revision Assert.AreEqual(1, attachments.Count()); var expected2 = new HashSet <BlobKey>(); expected2.AddItem(BlobStore.KeyForBlob(attach2)); Assert.AreEqual(expected2.Count, attachments.AllKeys().Count()); foreach (var key in attachments.AllKeys()) { Assert.IsTrue(expected2.Contains(key)); } }
internal void Open() { if (_isOpen) { return; } Log.D(TAG, "Opening {0}", Name); // Instantiate storage: //string storageType = Manager.StorageType ?? "SQLite"; Storage = new SqliteCouchStore(); Storage.Delegate = this; var encryptionKey = default(SymmetricKey); var gotKey = Manager.Shared.TryGetValue("encryptionKey", "", Name, out encryptionKey); if (gotKey) { Storage.SetEncryptionKey(encryptionKey); } Log.D(TAG, "Using {0} for db at {1}", Storage.GetType(), Path); try { Storage.Open(Path, Manager); // HACK: Needed to overcome the read connection not getting the write connection // changes until after the schema is written Storage.Close(); Storage.Open(Path, Manager); } catch(CouchbaseLiteException) { Storage.Close(); Log.W(TAG, "Error creating storage engine"); throw; } catch(Exception e) { throw new CouchbaseLiteException("Unknown exception creating storage engine", e) { Code = StatusCode.Exception }; } Storage.AutoCompact = AUTO_COMPACT; // First-time setup: if (PrivateUUID() == null) { Storage.SetInfo("privateUUID", Misc.CreateGUID()); Storage.SetInfo("publicUUID", Misc.CreateGUID()); } var savedMaxRevDepth = _maxRevTreeDepth != 0 ? _maxRevTreeDepth.ToString() : Storage.GetInfo("max_revs"); int maxRevTreeDepth = 0; if (savedMaxRevDepth != null && int.TryParse(savedMaxRevDepth, out maxRevTreeDepth)) { MaxRevTreeDepth = maxRevTreeDepth; } else { MaxRevTreeDepth = DEFAULT_MAX_REVS; } // Open attachment store: string attachmentsPath = AttachmentStorePath; try { Attachments = new BlobStore(attachmentsPath, encryptionKey); } catch(CouchbaseLiteException) { Log.E(TAG, "Error creating blob store at {0}", attachmentsPath); Storage.Close(); Storage = null; throw; } catch(Exception e) { Storage.Close(); Storage = null; throw new CouchbaseLiteException(String.Format("Unknown error creating blob store at {0}", attachmentsPath), e) { Code = StatusCode.Exception }; } _isOpen = true; }
internal void OpenWithOptions(DatabaseOptions options) { if (IsOpen) { return; } Log.D(TAG, "Opening {0}", Name); _readonly = _readonly || options.ReadOnly; // Instantiate storage: string storageType = options.StorageType ?? Manager.StorageType ?? DatabaseOptions.SQLITE_STORAGE; var className = String.Format("Couchbase.Lite.Store.{0}CouchStore", storageType); var primaryStorage = Type.GetType(className, false, true); var errorMessage = default(string); if (primaryStorage == null) { #if !FORESTDB if (storageType == DatabaseOptions.FORESTDB_STORAGE) { errorMessage = "ForestDB storage option selected but not compiled into library"; } #endif #if NOSQLITE if (storageType == DatabaseOptions.SQLITE_STORAGE) { errorMessage = "SQLite storage option selected but not compiled into library"; } #endif } else if (primaryStorage.GetInterface("Couchbase.Lite.Store.ICouchStore") == null) { errorMessage = String.Format("{0} does not implement ICouchStore", className); primaryStorage = null; } if (primaryStorage == null) { throw new CouchbaseLiteException(errorMessage, StatusCode.InvalidStorageType); } var upgrade = false; var primarySQLite = storageType == DatabaseOptions.SQLITE_STORAGE; var otherStorage = primarySQLite ? Type.GetType("Couchbase.Lite.Store.ForestDBCouchStore") : Type.GetType("Couchbase.Lite.Store.SqliteCouchStore"); var primaryStorageInstance = (ICouchStore)Activator.CreateInstance(primaryStorage); var otherStorageInstance = otherStorage != null ? (ICouchStore)Activator.CreateInstance(otherStorage) : null; if(options.StorageType != null) { // If explicit storage type given in options, always use primary storage type, // and if secondary db exists, try to upgrade from it: upgrade = otherStorageInstance != null && otherStorageInstance.DatabaseExistsIn(DbDirectory) && !primaryStorageInstance.DatabaseExistsIn(DbDirectory); if (upgrade && primarySQLite) { throw new CouchbaseLiteException("Cannot upgrade to SQLite", StatusCode.InvalidStorageType); } } else { // If options don't specify, use primary unless secondary db already exists in dir: if (otherStorageInstance != null && otherStorageInstance.DatabaseExistsIn(DbDirectory)) { primaryStorageInstance = otherStorageInstance; } } Log.I(TAG, "Using {0} for db at {1}; upgrade={2}", primaryStorage, DbDirectory, upgrade); Storage = primaryStorageInstance; Storage.Delegate = this; Storage.AutoCompact = AUTO_COMPACT; // Encryption: var encryptionKey = options.EncryptionKey; if (encryptionKey != null) { Storage.SetEncryptionKey(encryptionKey); } // Open the storage! try { Storage.Open(DbDirectory, Manager, _readonly); } catch(CouchbaseLiteException) { Storage.Close(); Log.E(TAG, "Failed to open storage for database"); throw; } catch(Exception e) { Storage.Close(); throw new CouchbaseLiteException("Error opening storage for database", e); } // First-time setup: if (PrivateUUID() == null) { Storage.SetInfo("privateUUID", Misc.CreateGUID()); Storage.SetInfo("publicUUID", Misc.CreateGUID()); } var savedMaxRevDepth = _maxRevTreeDepth != 0 ? _maxRevTreeDepth.ToString() : Storage.GetInfo("max_revs"); int maxRevTreeDepth = 0; if (savedMaxRevDepth != null && int.TryParse(savedMaxRevDepth, out maxRevTreeDepth)) { SetMaxRevTreeDepth(maxRevTreeDepth); } else { SetMaxRevTreeDepth(DEFAULT_MAX_REVS); } // Open attachment store: string attachmentsPath = AttachmentStorePath; try { Attachments = new BlobStore(attachmentsPath, encryptionKey); } catch(CouchbaseLiteException) { Log.E(TAG, "Error creating blob store at {0}", attachmentsPath); Storage.Close(); Storage = null; throw; } catch(Exception e) { Storage.Close(); Storage = null; throw new CouchbaseLiteException(String.Format("Unknown error creating blob store at {0}", attachmentsPath), e) { Code = StatusCode.Exception }; } IsOpen = true; if (upgrade) { var upgrader = DatabaseUpgraderFactory.CreateUpgrader(this, DbDirectory); try { upgrader.Import(); } catch(CouchbaseLiteException e) { Log.W(TAG, "Upgrade failed for {0} (Status {1})", DbDirectory, e.CBLStatus); upgrader.Backout(); Close(); throw; } } }
internal void Open() { if (_isOpen) { return; } Log.D(TAG, "Opening {0}", Name); // Instantiate storage: string storageType = Manager.StorageType ?? "SQLite"; #if !FORESTDB #if NOSQLITE #error No storage engine compilation options selected #endif if(storageType == "ForestDB") { throw new ApplicationException("ForestDB storage engine selected, but not compiled in library"); } #elif FORESTDB #if NOSQLITE if(storageType == "SQLite") { throw new ApplicationException("SQLite storage engine selected, but not compiled in library"); } #endif #endif var className = String.Format("Couchbase.Lite.Store.{0}CouchStore", storageType); var primaryStorage = Type.GetType(className, false, true); if(primaryStorage == null) { throw new InvalidOperationException(String.Format("'{0}' is not a valid storage type", storageType)); } var isStore = primaryStorage.GetInterface("Couchbase.Lite.Store.ICouchStore") != null; if(!isStore) { throw new InvalidOperationException(String.Format("{0} does not implement ICouchStore", className)); } #if !NOSQLITE #if FORESTDB var secondaryClass = "Couchbase.Lite.Store.ForestDBCouchStore"; if(className == secondaryClass) { secondaryClass = "Couchbase.Lite.Store.SqliteCouchStore"; } var secondaryStorage = Type.GetType(secondaryClass, false, true); Storage = (ICouchStore)Activator.CreateInstance(secondaryStorage); if(!Storage.DatabaseExistsIn(DbDirectory)) { #endif Storage = (ICouchStore)Activator.CreateInstance(primaryStorage); #if FORESTDB } #endif #else Storage = new ForestDBCouchStore(); #endif var encryptionKey = default(SymmetricKey); var gotKey = Manager.Shared.TryGetValue("encryptionKey", "", Name, out encryptionKey); if (gotKey) { Storage.SetEncryptionKey(encryptionKey); } Storage.Delegate = this; Log.D(TAG, "Using {0} for db at {1}", Storage.GetType(), DbDirectory); try { Storage.Open(DbDirectory, Manager, false); // HACK: Needed to overcome the read connection not getting the write connection // changes until after the schema is written Storage.Close(); Storage.Open(DbDirectory, Manager, false); } catch(CouchbaseLiteException) { Storage.Close(); Log.W(TAG, "Failed to create storage engine"); throw; } catch(Exception e) { throw new CouchbaseLiteException("Error creating storage engine", e) { Code = StatusCode.Exception }; } Storage.AutoCompact = AUTO_COMPACT; // First-time setup: if (PrivateUUID() == null) { Storage.SetInfo("privateUUID", Misc.CreateGUID()); Storage.SetInfo("publicUUID", Misc.CreateGUID()); } var savedMaxRevDepth = _maxRevTreeDepth != 0 ? _maxRevTreeDepth.ToString() : Storage.GetInfo("max_revs"); int maxRevTreeDepth = 0; if (savedMaxRevDepth != null && int.TryParse(savedMaxRevDepth, out maxRevTreeDepth)) { MaxRevTreeDepth = maxRevTreeDepth; } else { MaxRevTreeDepth = DEFAULT_MAX_REVS; } // Open attachment store: string attachmentsPath = AttachmentStorePath; try { Attachments = new BlobStore(attachmentsPath, encryptionKey); } catch(CouchbaseLiteException) { Log.E(TAG, "Error creating blob store at {0}", attachmentsPath); Storage.Close(); Storage = null; throw; } catch(Exception e) { Storage.Close(); Storage = null; throw new CouchbaseLiteException(String.Format("Unknown error creating blob store at {0}", attachmentsPath), e) { Code = StatusCode.Exception }; } _isOpen = true; }
internal Boolean Open() { if (_isOpen) { return true; } // Create the storage engine. StorageEngine = SQLiteStorageEngineFactory.CreateStorageEngine(); // Try to open the storage engine and stop if we fail. if (StorageEngine == null || !StorageEngine.Open(Path)) { var msg = "Unable to create a storage engine, fatal error"; Log.E(Tag, msg); throw new CouchbaseLiteException(msg); } // Stuff we need to initialize every time the sqliteDb opens: if (!Initialize("PRAGMA foreign_keys = ON;")) { Log.E(Tag, "Error turning on foreign keys"); return false; } // Check the user_version number we last stored in the sqliteDb: var dbVersion = StorageEngine.GetVersion(); // Incompatible version changes increment the hundreds' place: if (dbVersion >= 100) { Log.E(Tag, "Database: Database version (" + dbVersion + ") is newer than I know how to work with"); StorageEngine.Close(); return false; } if (dbVersion < 1) { // First-time initialization: // (Note: Declaring revs.sequence as AUTOINCREMENT means the values will always be // monotonically increasing, never reused. See <http://www.sqlite.org/autoinc.html>) if (!Initialize(Schema)) { StorageEngine.Close(); return false; } dbVersion = 3; } if (dbVersion < 2) { // Version 2: added attachments.revpos var upgradeSql = "ALTER TABLE attachments ADD COLUMN revpos INTEGER DEFAULT 0; PRAGMA user_version = 2"; if (!Initialize(upgradeSql)) { StorageEngine.Close(); return false; } dbVersion = 2; } if (dbVersion < 3) { var upgradeSql = "CREATE TABLE localdocs ( " + "docid TEXT UNIQUE NOT NULL, " + "revid TEXT NOT NULL, " + "json BLOB); " + "CREATE INDEX localdocs_by_docid ON localdocs(docid); " + "PRAGMA user_version = 3"; if (!Initialize(upgradeSql)) { StorageEngine.Close(); return false; } dbVersion = 3; } if (dbVersion < 4) { var upgradeSql = "CREATE TABLE info ( " + "key TEXT PRIMARY KEY, " + "value TEXT); " + "INSERT INTO INFO (key, value) VALUES ('privateUUID', '" + Misc.CreateGUID( ) + "'); " + "INSERT INTO INFO (key, value) VALUES ('publicUUID', '" + Misc.CreateGUID () + "'); " + "PRAGMA user_version = 4"; if (!Initialize(upgradeSql)) { StorageEngine.Close(); return false; } dbVersion = 4; } if (dbVersion < 5) { // Version 5: added encoding for attachments var upgradeSql = "ALTER TABLE attachments ADD COLUMN encoding INTEGER DEFAULT 0; " + "ALTER TABLE attachments ADD COLUMN encoded_length INTEGER; " + "PRAGMA user_version = 5"; if (!Initialize(upgradeSql)) { StorageEngine.Close(); return false; } dbVersion = 5; } if (dbVersion < 6) { // Version 6: enable Write-Ahead Log (WAL) <http://sqlite.org/wal.html> // Not supported on Android, require SQLite 3.7.0 //String upgradeSql = "PRAGMA journal_mode=WAL; " + var upgradeSql = "PRAGMA user_version = 6"; if (!Initialize(upgradeSql)) { StorageEngine.Close(); return false; } dbVersion = 6; } if (dbVersion < 7) { // Version 7: enable full-text search // Note: Apple's SQLite build does not support the icu or unicode61 tokenizers :( // OPT: Could add compress/decompress functions to make stored content smaller // Not supported on Android //String upgradeSql = "CREATE VIRTUAL TABLE fulltext USING fts4(content, tokenize=unicodesn); " + //"ALTER TABLE maps ADD COLUMN fulltext_id INTEGER; " + //"CREATE INDEX IF NOT EXISTS maps_by_fulltext ON maps(fulltext_id); " + //"CREATE TRIGGER del_fulltext DELETE ON maps WHEN old.fulltext_id not null " + //"BEGIN DELETE FROM fulltext WHERE rowid=old.fulltext_id| END; " + var upgradeSql = "PRAGMA user_version = 7"; if (!Initialize(upgradeSql)) { StorageEngine.Close(); return false; } dbVersion = 7; } // (Version 8 was an older version of the geo index) if (dbVersion < 9) { // Version 9: Add geo-query index //String upgradeSql = "CREATE VIRTUAL TABLE bboxes USING rtree(rowid, x0, x1, y0, y1); " + //"ALTER TABLE maps ADD COLUMN bbox_id INTEGER; " + //"ALTER TABLE maps ADD COLUMN geokey BLOB; " + //"CREATE TRIGGER del_bbox DELETE ON maps WHEN old.bbox_id not null " + //"BEGIN DELETE FROM bboxes WHERE rowid=old.bbox_id| END; " + var upgradeSql = "PRAGMA user_version = 9"; if (!Initialize(upgradeSql)) { StorageEngine.Close(); return false; } dbVersion = 9; } if (dbVersion < 10) { // Version 10: Add rev flag for whether it has an attachment var upgradeSql = "ALTER TABLE revs ADD COLUMN no_attachments BOOLEAN; " + "PRAGMA user_version = 10"; if (!Initialize(upgradeSql)) { StorageEngine.Close(); return false; } dbVersion = 10; } if (dbVersion < 11) { // Version 10: Add another index var upgradeSql = "CREATE INDEX revs_cur_deleted ON revs(current,deleted); " + "PRAGMA user_version = 11"; if (!Initialize(upgradeSql)) { StorageEngine.Close(); return false; } } try { Attachments = new BlobStore(AttachmentStorePath); } catch (ArgumentException e) { Log.E(Tag, "Could not initialize attachment store", e); StorageEngine.Close(); return false; } _isOpen = true; return true; }
public AtomicAction ActionToChangeEncryptionKey(SymmetricKey newKey) { var action = new AtomicAction(); // Find all the blob files: var blobs = default(string[]); var oldKey = EncryptionKey; blobs = Directory.GetFiles(_path, "*" + FileExtension); if (blobs.Length == 0) { // No blobs, so nothing to encrypt. Just add/remove the encryption marker file: action.AddLogic(() => { Log.D(TAG, "{0} {1}", (newKey != null) ? "encrypting" : "decrypting", _path); Log.D(TAG, " No blobs to copy; done."); EncryptionKey = newKey; MarkEncrypted(newKey != null); }, () => { EncryptionKey = oldKey; MarkEncrypted(oldKey != null); }, null); return(action); } // Create a new directory for the new blob store. Have to do this now, before starting the // action, because farther down we create an action to move it... var tempPath = Path.Combine(Path.GetTempPath(), String.Format("CouchbaseLite-Temp-{0}", Misc.CreateGUID())); action.AddLogic(() => { Log.D(TAG, "{0} {1}", (newKey != null) ? "encrypting" : "decrypting", _path); Directory.CreateDirectory(tempPath); }, () => Directory.Delete(tempPath, true), null); var tempStore = default(BlobStore); action.AddLogic(() => { tempStore = new BlobStore(tempPath, newKey); tempStore.MarkEncrypted(true); }, null, null); // Copy each of my blobs into the new store (which will update its encryption): action.AddLogic(() => { foreach (var blobName in blobs) { // Copy file by reading with old key and writing with new one: Log.D(TAG, " Copying {0}", blobName); Stream readStream = File.Open(blobName, FileMode.Open, FileAccess.Read, FileShare.Read); if (EncryptionKey != null) { readStream = EncryptionKey.DecryptStream(readStream); } var writer = new BlobStoreWriter(tempStore); try { writer.Read(readStream); writer.Finish(); writer.Install(); } catch (Exception) { writer.Cancel(); throw; } finally { readStream.Dispose(); } } }, null, null); // Replace the attachment dir with the new one: action.AddLogic(AtomicAction.MoveDirectory(tempPath, _path)); // Finally update EncryptionKey: action.AddLogic(() => { EncryptionKey = newKey; }, () => { EncryptionKey = oldKey; }, null); return(action); }
/// <exception cref="System.Exception"></exception> public virtual void TestAttachments() { string testAttachmentName = "test_attachment"; BlobStore attachments = database.GetAttachments(); NUnit.Framework.Assert.AreEqual(0, attachments.Count()); NUnit.Framework.Assert.AreEqual(new HashSet <object>(), attachments.AllKeys()); Status status = new Status(); IDictionary <string, object> rev1Properties = new Dictionary <string, object>(); rev1Properties.Put("foo", 1); rev1Properties.Put("bar", false); RevisionInternal rev1 = database.PutRevision(new RevisionInternal(rev1Properties, database), null, false, status); NUnit.Framework.Assert.AreEqual(Status.Created, status.GetCode()); byte[] attach1 = Sharpen.Runtime.GetBytesForString("This is the body of attach1"); database.InsertAttachmentForSequenceWithNameAndType(new ByteArrayInputStream(attach1 ), rev1.GetSequence(), testAttachmentName, "text/plain", rev1.GetGeneration()); NUnit.Framework.Assert.AreEqual(Status.Created, status.GetCode()); //We must set the no_attachments column for the rev to false, as we are using an internal //private API call above (database.insertAttachmentForSequenceWithNameAndType) which does //not set the no_attachments column on revs table try { ContentValues args = new ContentValues(); args.Put("no_attachments=", false); database.GetDatabase().Update("revs", args, "sequence=?", new string[] { rev1.GetSequence ().ToString() }); } catch (SQLException e) { Log.E(Database.Tag, "Error setting rev1 no_attachments to false", e); throw new CouchbaseLiteException(Status.InternalServerError); } Attachment attachment = database.GetAttachmentForSequence(rev1.GetSequence(), testAttachmentName ); NUnit.Framework.Assert.AreEqual("text/plain", attachment.GetContentType()); byte[] data = IOUtils.ToByteArray(attachment.GetContent()); NUnit.Framework.Assert.IsTrue(Arrays.Equals(attach1, data)); IDictionary <string, object> innerDict = new Dictionary <string, object>(); innerDict.Put("content_type", "text/plain"); innerDict.Put("digest", "sha1-gOHUOBmIMoDCrMuGyaLWzf1hQTE="); innerDict.Put("length", 27); innerDict.Put("stub", true); innerDict.Put("revpos", 1); IDictionary <string, object> attachmentDict = new Dictionary <string, object>(); attachmentDict.Put(testAttachmentName, innerDict); IDictionary <string, object> attachmentDictForSequence = database.GetAttachmentsDictForSequenceWithContent (rev1.GetSequence(), EnumSet.NoneOf <Database.TDContentOptions>()); NUnit.Framework.Assert.AreEqual(attachmentDict, attachmentDictForSequence); RevisionInternal gotRev1 = database.GetDocumentWithIDAndRev(rev1.GetDocId(), rev1 .GetRevId(), EnumSet.NoneOf <Database.TDContentOptions>()); IDictionary <string, object> gotAttachmentDict = (IDictionary <string, object>)gotRev1 .GetProperties().Get("_attachments"); NUnit.Framework.Assert.AreEqual(attachmentDict, gotAttachmentDict); // Check the attachment dict, with attachments included: Sharpen.Collections.Remove(innerDict, "stub"); innerDict.Put("data", Base64.EncodeBytes(attach1)); attachmentDictForSequence = database.GetAttachmentsDictForSequenceWithContent(rev1 .GetSequence(), EnumSet.Of(Database.TDContentOptions.TDIncludeAttachments)); NUnit.Framework.Assert.AreEqual(attachmentDict, attachmentDictForSequence); gotRev1 = database.GetDocumentWithIDAndRev(rev1.GetDocId(), rev1.GetRevId(), EnumSet .Of(Database.TDContentOptions.TDIncludeAttachments)); gotAttachmentDict = (IDictionary <string, object>)gotRev1.GetProperties().Get("_attachments" ); NUnit.Framework.Assert.AreEqual(attachmentDict, gotAttachmentDict); // Add a second revision that doesn't update the attachment: IDictionary <string, object> rev2Properties = new Dictionary <string, object>(); rev2Properties.Put("_id", rev1.GetDocId()); rev2Properties.Put("foo", 2); rev2Properties.Put("bazz", false); RevisionInternal rev2 = database.PutRevision(new RevisionInternal(rev2Properties, database), rev1.GetRevId(), false, status); NUnit.Framework.Assert.AreEqual(Status.Created, status.GetCode()); database.CopyAttachmentNamedFromSequenceToSequence(testAttachmentName, rev1.GetSequence (), rev2.GetSequence()); // Add a third revision of the same document: IDictionary <string, object> rev3Properties = new Dictionary <string, object>(); rev3Properties.Put("_id", rev2.GetDocId()); rev3Properties.Put("foo", 2); rev3Properties.Put("bazz", false); RevisionInternal rev3 = database.PutRevision(new RevisionInternal(rev3Properties, database), rev2.GetRevId(), false, status); NUnit.Framework.Assert.AreEqual(Status.Created, status.GetCode()); byte[] attach2 = Sharpen.Runtime.GetBytesForString("<html>And this is attach2</html>" ); database.InsertAttachmentForSequenceWithNameAndType(new ByteArrayInputStream(attach2 ), rev3.GetSequence(), testAttachmentName, "text/html", rev2.GetGeneration()); // Check the 2nd revision's attachment: Attachment attachment2 = database.GetAttachmentForSequence(rev2.GetSequence(), testAttachmentName ); NUnit.Framework.Assert.AreEqual("text/plain", attachment2.GetContentType()); data = IOUtils.ToByteArray(attachment2.GetContent()); NUnit.Framework.Assert.IsTrue(Arrays.Equals(attach1, data)); // Check the 3rd revision's attachment: Attachment attachment3 = database.GetAttachmentForSequence(rev3.GetSequence(), testAttachmentName ); NUnit.Framework.Assert.AreEqual("text/html", attachment3.GetContentType()); data = IOUtils.ToByteArray(attachment3.GetContent()); NUnit.Framework.Assert.IsTrue(Arrays.Equals(attach2, data)); IDictionary <string, object> attachmentDictForRev3 = (IDictionary <string, object>) database.GetAttachmentsDictForSequenceWithContent(rev3.GetSequence(), EnumSet.NoneOf <Database.TDContentOptions>()).Get(testAttachmentName); if (attachmentDictForRev3.ContainsKey("follows")) { if (((bool)attachmentDictForRev3.Get("follows")) == true) { throw new RuntimeException("Did not expected attachment dict 'follows' key to be true" ); } else { throw new RuntimeException("Did not expected attachment dict to have 'follows' key" ); } } // Examine the attachment store: NUnit.Framework.Assert.AreEqual(2, attachments.Count()); ICollection <BlobKey> expected = new HashSet <BlobKey>(); expected.AddItem(BlobStore.KeyForBlob(attach1)); expected.AddItem(BlobStore.KeyForBlob(attach2)); NUnit.Framework.Assert.AreEqual(expected, attachments.AllKeys()); database.Compact(); // This clears the body of the first revision NUnit.Framework.Assert.AreEqual(1, attachments.Count()); ICollection <BlobKey> expected2 = new HashSet <BlobKey>(); expected2.AddItem(BlobStore.KeyForBlob(attach2)); NUnit.Framework.Assert.AreEqual(expected2, attachments.AllKeys()); }