Trouble installing pre-built database using Xamarin and C#


#1

I’m using CouchbaseLite with Xamarin, and am following some example code from the CouchbaseConnect2014 project for creating a snapshot of our database with the intention of bundling it, and preloading it in our app on first run. I’m getting as far as loading the created database into our project, but am having trouble getting the attachments to come through.

When I try to retrieve an attachment, I am getting a InternalServerError exception. The generated cblite file seems to load ok, but when I try to retrieve an attachment from a particular document, I get the an exception when it tries to access the attachment’s content.

I import the cblite file and the attachments as embedded resources into my project, and access them with the Xamarin specific calls below. This “seems” to work fine, without error. I am getting valid stream objects.

Here is my code for loading the database.

var manager = Manager.SharedInstance;
Debug.Assert(manager != null, "Could not access manager");
if (manager.GetExistingDatabase (Config.Instance.DbName) != null) 
{
	Debug.WriteLine("Found existing local database");
	database = manager.GetDatabase (Config.Instance.DbName); 
} 
else 
{
	var resourceName = Assembly.GetAssembly (typeof(myappname.App)).GetManifestResourceNames ().Where (x => x.EndsWith ("baseline_db.cblite", StringComparison.CurrentCultureIgnoreCase)).Single ();

	Stream baselinedb = Assembly.GetAssembly (typeof(myappname.App)).GetManifestResourceStream (resourceName);

	// This returns all resources, because I don't know how to do linq queries correctly...
	var resources = Assembly.GetAssembly (typeof(myappname.App)).GetManifestResourceNames ().Where (x => x.EndsWith (".blob", StringComparison.CurrentCultureIgnoreCase));

	Dictionary<string, Stream> attachments = new Dictionary<string, Stream> ();

	foreach (string sz in resources) {
		if (sz.EndsWith (".blob")) {
			attachments.Add (sz, Assembly.GetAssembly (typeof(myappname.App)).GetManifestResourceStream (sz));
		}
	}

	Debug.WriteLine("Creating local database from baseline_db" );
	database = manager.GetDatabase (Config.Instance.DbName); 
	manager.ReplaceDatabase(Config.Instance.DbName, baselinedb,	attachments);
}

Here is what I am doing when I try to access the attachments.

public Attachment GetAttachment(string id, string name)
{
      if (id == null || id == string.Empty)
     {
            return null;
     }

	var doc = database.GetExistingDocument (id);

	if (doc == null)
	{
		return null;
	}

	var attachment = doc.CurrentRevision.GetAttachment (name);

	if (attachment == null || attachment.Metadata == null)
	{
		return null;
	}

	return attachment;
}   

var attachment = Couchbase.Instance.GetAttachment (Id, "cover.png");

if(attachment != null && attachment.Content != null) // Exception occurs here when Content is accessed
{
	_imageBytes = attachment.Content as byte[];
}

The attachment is partially filled out, but it’s Content and ContentStream members are null. I suspect it may not like the dictionary of streams that I pass in to the ReplaceDatabase function, but I was unable to find any documentation on what it expects. Am I doing anything obviously wrong? Do I manually need to copy my attachments to a specific location? I was assuming/hoping that the ReplaceDatabase call would take the streams and put them in the right spot. Any info appreciated.


#2

That API will copy the streams for you. You will get that error, however, if the attachment store cannot find the stream at the correct path for whatever reason. Could you be more specific about what you did to create a snapshot? Did you copy the streams as is from your snapshot or rename them in any way?

Edit: Could you also verify that the stream names you are getting do not contain your assembly name in them? They must simply be filename.blob not X.Y.Z.filename.blob


#3

Sure thing! Basically, I initialize our database, start a replication, and make a list of documents based on the database.Changed event handler. Once the replication is complete, I manually call the function below, where ‘database’ is the source db, and baseline_db is the target. My goal here was to ‘reset’ the revision number on these documents so it would be a fresh set of data without a history. Also, when I retained the _rev key, I got an exception about the mismatch. This may be the source of my problem, but I wasn’t sure what the correct way to handle it would be.

Once this is done, I manually grab the newly created baseline_db.cblite file and it’s corresponding baseline_db folder from my /.local/share folder, copy them into my project, and mark them as embedded resources. I don’t alter the names of the files, and I bring in the folder with it’s existing hierarchy. However, I did not add the baseline_db/attachments/temp_attachments folder into the project as it was empty.

public void MakeLoadableDB()
{
	foreach (string docId in docIds) 
	{
		var sourcedoc = database.GetExistingDocument(docId );
		if (sourcedoc == null)
		{
			Debug.WriteLine("Document retrieval failed for doc " + docId);
			continue;
		}

		var destdoc = baseline_db.GetDocument(sourcedoc.Id);
	
		var destdocrev = destdoc.CreateRevision();

		Dictionary<string, object> newprops = new Dictionary<string, object>();
		foreach(string key in sourcedoc.Properties.Keys)
		{
			if (key != "_rev")
				newprops.Add (key, sourcedoc.Properties [key]);
			//else
			//	newprops.Add ("_rev",	destdoc.CurrentRevisionId); 

		}
		//destdoc.PutProperties(newprops);
		destdocrev.SetProperties (newprops);

		foreach(Attachment a in sourcedoc.CurrentRevision.Attachments)
		{
			destdocrev.SetAttachment(a.Name, a.ContentType, a.Content);
			Debug.WriteLine("Added attachment [{0}] to document [{1}].", a.Name, destdoc.Id);
		}
		destdocrev.Save();
		Console.WriteLine ("Saving {0}", destdocrev.Id);

	}
}

#4

The dictionary entries in your attachments dictionary need to have keys that correspond to the correct filename. I think it is likely that you are changing “correct_name.blob” to “my.namespace.prefix.correct_name.blob” when you add them as embedded resources since that is what GetManifestResourceNames() returns. Could you check on that?


Unable to use installed database on Android
#5

That was it! I stripped out the prefix in the key name and everything seems to be working correctly. Thank you for your help and quick responses!


#6

Follow up question if you don’t mind. I’m seeing some strange behavior when I try to remove documents from the db. I thought it might be related to stripping out the _rev key when I create the snapshot, so I changed this code from:

to be just simply:

destdocrev.SetProperties (sourcedoc.Properties);

This seems to retain the revision info (and seems to be a more correct way to snapshot our database), but the strange behavior I’m seeing is this:

  1. I create a snapshot of the db as described above.
  2. I subsequently delete a document from the sync_gateway database.
  3. After I remove the document, I install our pre-built database in our
    application, but the item removed in step 2 is still there.

However, if I install the pre-built database in the app, and then subsequently delete the document (basically reverse steps 2 & 3), the item is removed as expected but the database reports a conflict.

I guess I’m just not sure what the “correct” way to do this is. We want to have a pre-built database bundled with our app, but also want to be able to change the sync_gateway database source if we need to without releasing a new version of the app with an updated bundled database. Any obvious step I may be missing?

Edit: It’s probably worth mentioning that if I don’t install the pre-built database, we are able to remove documents without any trouble and it all works as expected (though we only recently started logging conflicts, so I’m unsure as to whether or not we were getting conflicts when removing documents prior to these changes).


#7

I’ll see if I can reproduce this behavior sometime this week or next week and get back to you.


#8

Let me know if there is any other info I can provide. Thanks!


#9

It looks like this is a client side problem. We get a change notification for the deleted documents that tells us that the revision we have is not the current one, but we aren’t doing anything to handle document conflict resolution. Looking at the documentation now to figure out how we are supposed to handle that.


#10

Added the following to my database.Changed handler, and the documents are updating correctly.

database.Changed += (sender, e) => {
	foreach (DocumentChange change in e.Changes) 
	{
		if(change.IsCurrentRevision == false)
		{
			var changedoc = database.GetExistingDocument(change.DocumentId);
			changedoc.CurrentRevision.DeleteDocument();
		}
	}
};