How to extract and run a file during installation - wix

I have created a Custom Action (DTF) with C#.
In that CA, I would like to extract a file from the msi (declared as Binary in wix) and run it with some arguments.
I haven't found any samples or help about that..
I have to execute a request on the msi, but I would like to have a sample. Thanks!

The DTF.chm has a sample how to update the Binary table. It's in "Working with MSI Databases" topic. And you can guess how to do the opposite operation. The code might look like this:
using (var db = new Database("test.msi", DatabaseOpenMode.Direct))
{
using (var view = db.OpenView("SELECT `Data` FROM `Binary` WHERE `Name` = '{0}'", "testbinary"))
{
view.Execute();
var rec = view.Fetch();
var inStream = rec.GetStream("Data");
if (inStream != null)
{
using (var file = File.OpenWrite("S:\\testfile.zip"))
{
CopyStream(inStream, file);
}
}
}
}
The code of CopyStream method can be taken from this answer of omnipresent Jon Skeet. Note that if you should do this from CA, you will reference the database object like session.Database, instead of creating it.

Related

Renaming models in database with existing data when using RavenDb

Is there any 'easy' way to rename models in RavenDb when the database already has existing data? I have various models which were originally created in another language, and now I would like to rename them to English as the codebase is becoming quite unmaintainable. If I just rename them, then the data won't be loaded because the properties don't match anymore.
I would like the system to automatically do it on first load. Is there any best way how to approach this? My solution would be:
Check if a document exists to determine if the upgrade has been done or not
If upgrade has not been done, execute patch scripts to update fields
Update document to know that the upgrade has been done
I'd recommend you create new documents from the old documents.
This can be done pretty easily using patching via docStore.UpdateByIndex.
Suppose I had an old type name, Foo, and wanted to rename it to the new type name, Bar. And I wanted all the IDs to change from Foos/123 to Bars/123.
It would look something like this:
var patchScript = #"
// Copy all the properties from the old document
var newDoc = {};
for (var prop in this) {
if (prop !== '#metadata') {
newDoc[prop] = this[prop];
}
}
// Create the metadata.
var meta = {};
meta['Raven-Entity-Name'] = newCollection;
meta['Raven-Clr-Type'] = newType;
// Store the new document.
var newId = __document_id.replace(oldCollection, newCollection);
PutDocument(newId, newDoc, meta);
";
var oldCollection = "Foos";
var newCollection = "Bars";
var newType = "KarlCassar.Bar, KarlCassar"; // Where KarlCassar is your assembly name.
var query = new IndexQuery { Query = $"Tag:{oldCollection}" };
var options = new BulkOperationOptions { AllowStale = false };
var patch = new ScriptedPatchRequest
{
Script = patchScript,
Values = new Dictionary<string, object>
{
{ nameof(oldCollection), oldCollection },
{ nameof(newCollection), newCollection },
{ nameof(newType), newType }
}
};
var patchOperation = docStore.DatabaseCommands.UpdateByIndex("Raven/DocumentsByEntityName", query, patch, options);
patchOperation.WaitForCompletion();
Run that code once at startup, and then your app will be able to work with the new name entities. Your old entities are still around - those can be safely deleted via the Studio.

Update Document with external object

i have a database containing Song objects. The song class has > 30 properties.
My Music Tagging application is doing changes on a song on the file system.
It then does a lookup in the database using the filename.
Now i have a Song object, which i created in my Tagging application by reading the physical file and i have a Song object, which i have just retrieved from the database and which i want to update.
I thought i just could grab the ID from the database object, replace the database object with my local song object, set the saved id and store it.
But Raven claims that i am replacing the object with a different object.
Do i really need to copy every single property over, like this?
dbSong.Artist = songfromFilesystem.Artist;
dbSong.Album = songfromFileSystem.Album;
Or are there other possibilities.
thanks,
Helmut
Edit:
I was a bit too positive. The suggestion below works only in a test program.
When doing it in my original code i get following exception:
Attempted to associate a different object with id 'TrackDatas/3452'
This is produced by following code:
try
{
originalFileName = Util.EscapeDatabaseQuery(originalFileName);
// Lookup the track in the database
var dbTracks = _session.Advanced.DocumentQuery<TrackData, DefaultSearchIndex>().WhereEquals("Query", originalFileName).ToList();
if (dbTracks.Count > 0)
{
track.Id = dbTracks[0].Id;
_session.Store(track);
_session.SaveChanges();
}
}
catch (Exception ex)
{
log.Error("UpdateTrack: Error updating track in database {0}: {1}", ex.Message, ex.InnerException);
}
I am first looking up a song in the database and get a TrackData object in dbTracks.
The track object is also of type TrackData and i just put the ID from the object just retrieved and try to store it, which gives the above error.
I would think that the above message tells me that the objects are of different types, which they aren't.
The same error happens, if i use AutoMapper.
any idea?
You can do what you're trying: replace an existing object using just the ID. If it's not working, you might be doing something else wrong. (In which case, please show us your code.)
When it comes to updating existing objects in Raven, there are a few options:
Option 1: Just save the object using the same ID as an existing object:
var song = ... // load it from the file system or whatever
song.Id = "Songs/5"; // Set it to an existing song ID
DbSession.Store(song); // Overwrites the existing song
Option 2: Manually update the properties of the existing object.
var song = ...;
var existingSong = DbSession.Load<Song>("Songs/5");
existingSong.Artist = song.Artist;
existingSong.Album = song.Album;
Option 3: Dynamically update the existing object:
var song = ...;
var existingSong = DbSession.Load<Song>("Songs/5");
existingSong.CopyFrom(song);
Where you've got some code like this:
// Inside Song.cs
public virtual void CopyFrom(Song other)
{
var props = typeof(Song)
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
.Where(p => p.CanWrite);
foreach (var prop in props)
{
var source = prop.GetValue(other);
prop.SetValue(this, source);
}
}
If you find yourself having to do this often, use a library like AutoMapper.
Automapper can automatically copy one object to another with a single line of code.
Now that you've posted some code, I see 2 things:
First, is there a reason you're using the Advanced.DocumentQuery syntax?
// This is advanced query syntax. Is there a reason you're using it?
var dbTracks = _session.Advanced.DocumentQuery<TrackData, DefaultSearchIndex>().WhereEquals("Query", originalFileName).ToList();
Here's how I'd write your code using standard LINQ syntax:
var escapedFileName = Util.EscapeDatabaseQuery(originalFileName);
// Find the ID of the existing track in the database.
var existingTrackId = _session.Query<TrackData, DefaultSearchIndex>()
.Where(t => t.Query == escapedFileName)
.Select(t => t.Id);
if (existingTrackId != null)
{
track.Id = existingTrackId;
_session.Store(track);
_session.SaveChanges();
}
Finally, #2: what is track? Was it loaded via session.Load or session.Query? If so, that's not going to work, and it's causing your problem. If track is loaded from the database, you'll need to create a new object and save that:
var escapedFileName = Util.EscapeDatabaseQuery(originalFileName);
// Find the ID of the existing track in the database.
var existingTrackId = _session.Query<TrackData, DefaultSearchIndex>()
.Where(t => t.Query == escapedFileName)
.Select(t => t.Id);
if (existingTrackId != null)
{
var newTrack = new Track(...);
newTrack.Id = existingTrackId;
_session.Store(newTrack);
_session.SaveChanges();
}
This means you already have a different object in the session with the same id. The fix for me was to use a new session.

How to get local path for payload in WiX/Burn Managed Bootstrapper Application?

I am currently working in a WiX/Burn Managed Bootstrapper Application and cannot figure out how to get the local path for a payload (MSI).
I let the user select which applications they want to install in my custom UI, and I want to not show applications for which the MSI is missing. I also need to see information in the MSI's database.
I know I can determine missing payloads by handling "ResolveSource" but that doesn't happen until right before the application in installed.
I deserialize the BootstrapperApplicationData.xml file first thing so I have information about which MSIs MIGHT be installed, but it still doesn't help me determine the source of the MSIs.
Does anyone know how to determine the local path to a payload?
EDIT: Here is an example for how I reference all the installers:
<MsiPackage Id="AppName"
SourceFile="$(var.ProjectName.TargetDir)ProjectName.msi"
Name="MSI\ProjectName.msi"
Compressed="no"/>
In the GetLastUsedSourceFolder function in cache.cpp, you can see that the engine gets the source folder from the WixBundleLastUsedSource variable, and the parent directory of the WixBundleOriginalSource variable if WixBundleLastUsedSource isn't set.
You can use this along with the Name attribute of the WixPayloadProperties element in the BootstrapperApplicationData.xml file to predetermine where the engine will look for a payload. Note that the engine will actually look in the cache first.
The MSI files are embedded into the bundle .exe and aren't extracted from the bundle until right before the application is installed, which corresponds to when the ResolveSource event fires. However, if you really want to get this information, you can programatically extract the MSI files yourself and inspect them using the WiX DTF library (wix.dll in the /bin folder of your WiX install).
using Microsoft.Tools.WindowsInstallerXml;
private void ExtractEmbeddedMsiInstallers()
{
var tmpFolder = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
var bundlePath = Engine.StringVariables["WixBundleOriginalSource"];
Unbinder unbinder = null;
try
{
unbinder = new Unbinder();
//The next line will extract the MSIs into the tmpFolder in a subfolder named "AttachedContainer"
unbinder.Unbind(bundlePath, OutputType.Bundle, tmpFolder);
}
finally
{
if (null != unbinder)
unbinder.DeleteTempFiles();
}
}
You also mentioned needing to inspect data in the MSI database. Here's a sample of how to do that:
using (var database = new InstallPackage(msiFilePath, DatabaseOpenMode.Transact) { WorkingDirectory = _someTempFolder })
{
if (database.Tables.Contains("CustomAction"))
{
using (View view = database.OpenView("SELECT `Action`, `Type`, `Source`, `Target` FROM `CustomAction`"))
{
view.Execute();
foreach (Record rowRecord in view)
using (rowRecord)
{
var actionName = rowRecord.GetString(1);
var actionType = rowRecord.GetInteger(2);
var binaryName = rowRecord.GetString(3);
var methodName = rowRecord.GetString(4);
//Do something with the values
}
}
}
}

Check if a file exists in the project in WinRT

I have a WinRT Metro project which displays images based on a selected item. However, some of the images selected will not exist. What I want to be able to do is trap the case where they don't exist and display an alternative.
Here is my code so far:
internal string GetMyImage(string imageDescription)
{
string myImage = string.Format("Assets/MyImages/{0}.jpg", imageDescription.Replace(" ", ""));
// Need to check here if the above asset actually exists
return myImage;
}
Example calls:
GetMyImage("First Picture");
GetMyImage("Second Picture");
So Assets/MyImages/SecondPicture.jpg exists, but Assets/MyImages/FirstPicture.jpg does not.
At first I thought of using the WinRT equivalent of File.Exists(), but there doesn't appear to be one. Without having to go to the extent of trying to open the file and catching an error, can I simply check if either the file exists, or the file exists in the project?
You could use GetFilesAsync from here to enumerate the existing files. This seems to make sense considering you have multiple files which might not exist.
Gets a list of all files in the current folder and its sub-folders. Files are filtered and sorted based on the specified CommonFileQuery.
var folder = await StorageFolder.GetFolderFromPathAsync("Assets/MyImages/");
var files = await folder.GetFilesAsync(CommonFileQuery.OrderByName);
var file = files.FirstOrDefault(x => x.Name == "fileName");
if (file != null)
{
//do stuff
}
Edit:
As #Filip Skakun pointed out, the resource manager has a resource mapping on which you can call ContainsKey which has the benefit of checking for qualified resources as well (i.e. localized, scaled etc).
Edit 2:
Windows 8.1 introduced a new method for getting files and folders:
var result = await ApplicationData.Current.LocalFolder.TryGetItemAsync("fileName") as IStorageFile;
if (result != null)
//file exists
else
//file doesn't exist
There's two ways you can handle it.
1) Catch the FileNotFoundException when trying to get the file:
Windows.Storage.StorageFolder installedLocation =
Windows.ApplicationModel.Package.Current.InstalledLocation;
try
{
// Don't forget to decorate your method or event with async when using await
var file = await installedLocation.GetFileAsync(fileName);
// Exception wasn't raised, therefore the file exists
System.Diagnostics.Debug.WriteLine("We have the file!");
}
catch (System.IO.FileNotFoundException fileNotFoundEx)
{
System.Diagnostics.Debug.WriteLine("File doesn't exist. Use default.");
}
catch (Exception ex)
{
// Handle unknown error
}
2) as mydogisbox recommends, using LINQ. Although the method I tested is slightly different:
Windows.Storage.StorageFolder installedLocation =
Windows.ApplicationModel.Package.Current.InstalledLocation;
var files = await installedLocation.GetFilesAsync(CommonFileQuery.OrderByName);
var file = files.FirstOrDefault(x => x.Name == fileName);
if (file != null)
{
System.Diagnostics.Debug.WriteLine("We have the file!");
}
else
{
System.Diagnostics.Debug.WriteLine("No File. Use default.");
}
BitmapImage has an ImageFailed event that fires if the image can't be loaded. This would let you try to load the original image, and then react if it's not there.
Of course, this requires that you instantiate the BitmapImage yourself, rather than just build the Uri.
Sample checking for resource availability for c++ /cx (tested with Windows Phone 8.1):
std::wstring resPath = L"Img/my.bmp";
std::wstring resKey = L"Files/" + resPath;
bool exists = Windows::ApplicationModel::Resources::Core::ResourceManager::Current->MainResourceMap->HasKey(ref new Platform::String(resKey.c_str()));

How to Read a pre-built Text File in a Windows Phone Application

I've been trying to read a pre-built file with Car Maintenance tips, there's one in each line of my "Tips.txt" file. I've tried to follow around 4 or 5 different approaches but It's not working, it compiles but I get an exception. Here's what I've got:
using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication())
{
using (StreamReader sr = new StreamReader(store.OpenFile("Tips.txt", FileMode.Open, FileAccess.Read)))
{
string line;
while ((line = sr.ReadLine()) != null)
{
(App.Current as App).MyTips.Insert(new DoubleNode(line));
}
}
}
I'm getting this "Operation not permitted on IsolatedStorageFileStream", from the info inside the 2nd using statement. I tried with the build action of my "Tips.txt" set to resource, and content, yet I get the same result.
Thanks in advance.
Since you've added it to your project directory, you can't read it using Isolated Storage methods. There are various ways you can load the file. One way would be to set the text file's build type to Resource, then read it in as a stream:
//Replace 'MyProject' with the name of your XAP/Project
Stream txtStream = Application.GetResourceStream(new Uri("/MyProject;component/myTextFile.txt",
UriKind.Relative)).Stream;
using(StreamReader sr = new StreamReader(txtStream))
{
//your code
}