aboutsummaryrefslogtreecommitdiff
path: root/src/dtf/WixToolset.Dtf.WindowsInstaller/Database.cs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/Database.cs933
1 files changed, 933 insertions, 0 deletions
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/Database.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/Database.cs
new file mode 100644
index 00000000..09627f4b
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/Database.cs
@@ -0,0 +1,933 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.IO;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using System.Diagnostics.CodeAnalysis;
10
11 /// <summary>
12 /// Accesses a Windows Installer database.
13 /// </summary>
14 /// <remarks><p>
15 /// The <see cref="Commit"/> method must be called before the Database is closed to write out all
16 /// persistent changes. If the Commit method is not called, the installer performs an implicit
17 /// rollback upon object destruction.
18 /// </p><p>
19 /// The client can use the following procedure for data access:<list type="number">
20 /// <item><description>Obtain a Database object using one of the Database constructors.</description></item>
21 /// <item><description>Initiate a query using a SQL string by calling the <see cref="OpenView"/>
22 /// method of the Database.</description></item>
23 /// <item><description>Set query parameters in a <see cref="Record"/> and execute the database
24 /// query by calling the <see cref="View.Execute(Record)"/> method of the <see cref="View"/>. This
25 /// produces a result that can be fetched or updated.</description></item>
26 /// <item><description>Call the <see cref="View.Fetch"/> method of the View repeatedly to return
27 /// Records.</description></item>
28 /// <item><description>Update database rows of a Record object obtained by the Fetch method using
29 /// one of the <see cref="View.Modify"/> methods of the View.</description></item>
30 /// <item><description>Release the query and any unfetched records by calling the <see cref="InstallerHandle.Close"/>
31 /// method of the View.</description></item>
32 /// <item><description>Persist any database updates by calling the Commit method of the Database.
33 /// </description></item>
34 /// </list>
35 /// </p></remarks>
36 public partial class Database : InstallerHandle
37 {
38 private string filePath;
39 private DatabaseOpenMode openMode;
40 private SummaryInfo summaryInfo;
41 private TableCollection tables;
42 private IList<string> deleteOnClose;
43
44 /// <summary>
45 /// Opens an existing database in read-only mode.
46 /// </summary>
47 /// <param name="filePath">Path to the database file.</param>
48 /// <exception cref="InstallerException">the database could not be created/opened</exception>
49 /// <remarks><p>
50 /// Because this constructor initiates database access, it cannot be used with a
51 /// running installation.
52 /// </p><p>
53 /// The Database object should be <see cref="InstallerHandle.Close"/>d after use.
54 /// It is best that the handle be closed manually as soon as it is no longer
55 /// needed, as leaving lots of unused handles open can degrade performance.
56 /// </p><p>
57 /// Win32 MSI API:
58 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiopendatabase.asp">MsiOpenDatabase</a>
59 /// </p></remarks>
60 public Database(string filePath)
61 : this(filePath, DatabaseOpenMode.ReadOnly)
62 {
63 }
64
65 /// <summary>
66 /// Opens an existing database with another database as output.
67 /// </summary>
68 /// <param name="filePath">Path to the database to be read.</param>
69 /// <param name="outputPath">Open mode for the database</param>
70 /// <returns>Database object representing the created or opened database</returns>
71 /// <exception cref="InstallerException">the database could not be created/opened</exception>
72 /// <remarks><p>
73 /// When a database is opened as the output of another database, the summary information stream
74 /// of the output database is actually a read-only mirror of the original database and thus cannot
75 /// be changed. Additionally, it is not persisted with the database. To create or modify the
76 /// summary information for the output database it must be closed and re-opened.
77 /// </p><p>
78 /// The Database object should be <see cref="InstallerHandle.Close"/>d after use.
79 /// It is best that the handle be closed manually as soon as it is no longer
80 /// needed, as leaving lots of unused handles open can degrade performance.
81 /// </p><p>
82 /// The database is opened in <see cref="DatabaseOpenMode.CreateDirect" /> mode, and will be
83 /// automatically commited when it is closed.
84 /// </p><p>
85 /// Win32 MSI API:
86 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiopendatabase.asp">MsiOpenDatabase</a>
87 /// </p></remarks>
88 public Database(string filePath, string outputPath)
89 : this((IntPtr) Database.Open(filePath, outputPath), true, outputPath, DatabaseOpenMode.CreateDirect)
90 {
91 }
92
93 /// <summary>
94 /// Opens an existing database or creates a new one.
95 /// </summary>
96 /// <param name="filePath">Path to the database file. If an empty string
97 /// is supplied, a temporary database is created that is not persisted.</param>
98 /// <param name="mode">Open mode for the database</param>
99 /// <exception cref="InstallerException">the database could not be created/opened</exception>
100 /// <remarks><p>
101 /// Because this constructor initiates database access, it cannot be used with a
102 /// running installation.
103 /// </p><p>
104 /// The database object should be <see cref="InstallerHandle.Close"/>d after use.
105 /// The finalizer will close the handle if it is still open, however due to the nondeterministic
106 /// nature of finalization it is best that the handle be closed manually as soon as it is no
107 /// longer needed, as leaving lots of unused handles open can degrade performance.
108 /// </p><p>
109 /// A database opened in <see cref="DatabaseOpenMode.CreateDirect" /> or
110 /// <see cref="DatabaseOpenMode.Direct" /> mode will be automatically commited when it is
111 /// closed. However a database opened in <see cref="DatabaseOpenMode.Create" /> or
112 /// <see cref="DatabaseOpenMode.Transact" /> mode must have the <see cref="Commit" /> method
113 /// called before it is closed, otherwise no changes will be persisted.
114 /// </p><p>
115 /// Win32 MSI API:
116 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiopendatabase.asp">MsiOpenDatabase</a>
117 /// </p></remarks>
118 public Database(string filePath, DatabaseOpenMode mode)
119 : this((IntPtr) Database.Open(filePath, mode), true, filePath, mode)
120 {
121 }
122
123 /// <summary>
124 /// Creates a new database from an MSI handle.
125 /// </summary>
126 /// <param name="handle">Native MSI database handle.</param>
127 /// <param name="ownsHandle">True if the handle should be closed
128 /// when the database object is disposed</param>
129 /// <param name="filePath">Path of the database file, if known</param>
130 /// <param name="openMode">Mode the handle was originally opened in</param>
131 protected internal Database(
132 IntPtr handle, bool ownsHandle, string filePath, DatabaseOpenMode openMode)
133 : base(handle, ownsHandle)
134 {
135 this.filePath = filePath;
136 this.openMode = openMode;
137 }
138
139 /// <summary>
140 /// Gets the file path the Database was originally opened from, or null if not known.
141 /// </summary>
142 public String FilePath
143 {
144 get
145 {
146 return this.filePath;
147 }
148 }
149
150 /// <summary>
151 /// Gets the open mode for the database.
152 /// </summary>
153 public DatabaseOpenMode OpenMode
154 {
155 get
156 {
157 return this.openMode;
158 }
159 }
160
161 /// <summary>
162 /// Gets a boolean value indicating whether this database was opened in read-only mode.
163 /// </summary>
164 /// <remarks><p>
165 /// Win32 MSI API:
166 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetdatabasestate.asp">MsiGetDatabaseState</a>
167 /// </p></remarks>
168 public bool IsReadOnly
169 {
170 get
171 {
172 if (RemotableNativeMethods.RemotingEnabled)
173 {
174 return true;
175 }
176
177 int state = NativeMethods.MsiGetDatabaseState((int) this.Handle);
178 return state != 1;
179 }
180 }
181
182 /// <summary>
183 /// Gets the collection of tables in the Database.
184 /// </summary>
185 public TableCollection Tables
186 {
187 get
188 {
189 if (this.tables == null)
190 {
191 this.tables = new TableCollection(this);
192 }
193 return this.tables;
194 }
195 }
196
197 /// <summary>
198 /// Gets or sets the code page of the Database.
199 /// </summary>
200 /// <exception cref="IOException">error exporting/importing the codepage data</exception>
201 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
202 /// <remarks><p>
203 /// Getting or setting the code page is a slow operation because it involves an export or import
204 /// of the codepage data to/from a temporary file.
205 /// </p></remarks>
206 public int CodePage
207 {
208 get
209 {
210 string tempFile = Path.GetTempFileName();
211 StreamReader reader = null;
212 try
213 {
214 this.Export("_ForceCodepage", tempFile);
215 reader = File.OpenText(tempFile);
216 reader.ReadLine(); // Skip column name record.
217 reader.ReadLine(); // Skip column defn record.
218 string codePageLine = reader.ReadLine();
219 return Int32.Parse(codePageLine.Split('\t')[0], CultureInfo.InvariantCulture.NumberFormat);
220 }
221 finally
222 {
223 if (reader != null) reader.Close();
224 File.Delete(tempFile);
225 }
226 }
227
228 set
229 {
230 string tempFile = Path.GetTempFileName();
231 StreamWriter writer = null;
232 try
233 {
234 writer = File.AppendText(tempFile);
235 writer.WriteLine("");
236 writer.WriteLine("");
237 writer.WriteLine("{0}\t_ForceCodepage", value);
238 writer.Close();
239 writer = null;
240 this.Import(tempFile);
241 }
242 finally
243 {
244 if (writer != null) writer.Close();
245 File.Delete(tempFile);
246 }
247 }
248 }
249
250 /// <summary>
251 /// Gets the SummaryInfo object for this database that can be used to examine and modify properties
252 /// to the summary information stream.
253 /// </summary>
254 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
255 /// <remarks><p>
256 /// The object returned from this property does not need to be explicitly persisted or closed.
257 /// Any modifications will be automatically saved when the database is committed.
258 /// </p><p>
259 /// Win32 MSI API:
260 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetsummaryinformation.asp">MsiGetSummaryInformation</a>
261 /// </p></remarks>
262 public SummaryInfo SummaryInfo
263 {
264 get
265 {
266 if (this.summaryInfo == null || this.summaryInfo.IsClosed)
267 {
268 lock (this.Sync)
269 {
270 if (this.summaryInfo == null || this.summaryInfo.IsClosed)
271 {
272 int summaryInfoHandle;
273 int maxProperties = this.IsReadOnly ? 0 : SummaryInfo.MAX_PROPERTIES;
274 uint ret = RemotableNativeMethods.MsiGetSummaryInformation((int) this.Handle, null, (uint) maxProperties, out summaryInfoHandle);
275 if (ret != 0)
276 {
277 throw InstallerException.ExceptionFromReturnCode(ret);
278 }
279 this.summaryInfo = new SummaryInfo((IntPtr) summaryInfoHandle, true);
280 }
281 }
282 }
283 return this.summaryInfo;
284 }
285 }
286
287 /// <summary>
288 /// Creates a new Database object from an integer database handle.
289 /// </summary>
290 /// <remarks><p>
291 /// This method is only provided for interop purposes. A Database object
292 /// should normally be obtained from <see cref="Session.Database"/> or
293 /// a public Database constructor.
294 /// </p></remarks>
295 /// <param name="handle">Integer database handle</param>
296 /// <param name="ownsHandle">true to close the handle when this object is disposed</param>
297 public static Database FromHandle(IntPtr handle, bool ownsHandle)
298 {
299 return new Database(
300 handle,
301 ownsHandle,
302 null,
303 NativeMethods.MsiGetDatabaseState((int) handle) == 1 ? DatabaseOpenMode.Direct : DatabaseOpenMode.ReadOnly);
304 }
305
306 /// <summary>
307 /// Schedules a file or directory for deletion after the database handle is closed.
308 /// </summary>
309 /// <param name="path">File or directory path to be deleted. All files and subdirectories
310 /// under a directory are deleted.</param>
311 /// <remarks><p>
312 /// Once an item is scheduled, it cannot be unscheduled.
313 /// </p><p>
314 /// The items cannot be deleted if the Database object is auto-disposed by the
315 /// garbage collector; the handle must be explicitly closed.
316 /// </p><p>
317 /// Files which are read-only or otherwise locked cannot be deleted,
318 /// but they will not cause an exception to be thrown.
319 /// </p></remarks>
320 public void DeleteOnClose(string path)
321 {
322 if (this.deleteOnClose == null)
323 {
324 this.deleteOnClose = new List<string>();
325 }
326 this.deleteOnClose.Add(path);
327 }
328
329 /// <summary>
330 /// Merges another database with this database.
331 /// </summary>
332 /// <param name="otherDatabase">The database to be merged into this database</param>
333 /// <param name="errorTable">Optional name of table to contain the names of the tables containing
334 /// merge conflicts, the number of conflicting rows within the table, and a reference to the table
335 /// with the merge conflict.</param>
336 /// <exception cref="MergeException">merge failed due to a schema difference or data conflict</exception>
337 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
338 /// <remarks><p>
339 /// Merge does not copy over embedded cabinet files or embedded transforms from the
340 /// reference database into the target database. Embedded data streams that are listed in the
341 /// Binary table or Icon table are copied from the reference database to the target database.
342 /// Storage embedded in the reference database are not copied to the target database.
343 /// </p><p>
344 /// The Merge method merges the data of two databases. These databases must have the same
345 /// codepage. The merge fails if any tables or rows in the databases conflict. A conflict exists
346 /// if the data in any row in the first database differs from the data in the corresponding row
347 /// of the second database. Corresponding rows are in the same table of both databases and have
348 /// the same primary key in both databases. The tables of non-conflicting databases must have
349 /// the same number of primary keys, same number of columns, same column types, same column names,
350 /// and the same data in rows with identical primary keys. Temporary columns however don't matter
351 /// in the column count and corresponding tables can have a different number of temporary columns
352 /// without creating conflict as long as the persistent columns match.
353 /// </p><p>
354 /// If the number, type, or name of columns in corresponding tables are different, the
355 /// schema of the two databases are incompatible and the installer will stop processing tables
356 /// and the merge fails. The installer checks that the two databases have the same schema before
357 /// checking for row merge conflicts. If the schemas are incompatible, the databases have be
358 /// modified.
359 /// </p><p>
360 /// If the data in particular rows differ, this is a row merge conflict, the merge fails
361 /// and creates a new table with the specified name. The first column of this table is the name
362 /// of the table having the conflict. The second column gives the number of rows in the table
363 /// having the conflict.
364 /// </p><p>
365 /// Win32 MSI API:
366 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabasemerge.asp">MsiDatabaseMerge</a>
367 /// </p></remarks>
368 [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
369 public void Merge(Database otherDatabase, string errorTable)
370 {
371 if (otherDatabase == null)
372 {
373 throw new ArgumentNullException("otherDatabase");
374 }
375
376 uint ret = NativeMethods.MsiDatabaseMerge((int) this.Handle, (int) otherDatabase.Handle, errorTable);
377 if (ret != 0)
378 {
379 if (ret == (uint) NativeMethods.Error.FUNCTION_FAILED)
380 {
381 throw new MergeException(this, errorTable);
382 }
383 else if (ret == (uint) NativeMethods.Error.DATATYPE_MISMATCH)
384 {
385 throw new MergeException("Schema difference between the two databases.");
386 }
387 else
388 {
389 throw InstallerException.ExceptionFromReturnCode(ret);
390 }
391 }
392 }
393
394 /// <summary>
395 /// Merges another database with this database.
396 /// </summary>
397 /// <param name="otherDatabase">The database to be merged into this database</param>
398 /// <exception cref="MergeException">merge failed due to a schema difference or data conflict</exception>
399 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
400 /// <remarks><p>
401 /// MsiDatabaseMerge does not copy over embedded cabinet files or embedded transforms from
402 /// the reference database into the target database. Embedded data streams that are listed in
403 /// the Binary table or Icon table are copied from the reference database to the target database.
404 /// Storage embedded in the reference database are not copied to the target database.
405 /// </p><p>
406 /// The Merge method merges the data of two databases. These databases must have the same
407 /// codepage. The merge fails if any tables or rows in the databases conflict. A conflict exists
408 /// if the data in any row in the first database differs from the data in the corresponding row
409 /// of the second database. Corresponding rows are in the same table of both databases and have
410 /// the same primary key in both databases. The tables of non-conflicting databases must have
411 /// the same number of primary keys, same number of columns, same column types, same column names,
412 /// and the same data in rows with identical primary keys. Temporary columns however don't matter
413 /// in the column count and corresponding tables can have a different number of temporary columns
414 /// without creating conflict as long as the persistent columns match.
415 /// </p><p>
416 /// If the number, type, or name of columns in corresponding tables are different, the
417 /// schema of the two databases are incompatible and the installer will stop processing tables
418 /// and the merge fails. The installer checks that the two databases have the same schema before
419 /// checking for row merge conflicts. If the schemas are incompatible, the databases have be
420 /// modified.
421 /// </p><p>
422 /// Win32 MSI API:
423 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabasemerge.asp">MsiDatabaseMerge</a>
424 /// </p></remarks>
425 public void Merge(Database otherDatabase) { this.Merge(otherDatabase, null); }
426
427 /// <summary>
428 /// Checks whether a table exists and is persistent in the database.
429 /// </summary>
430 /// <param name="table">The table to the checked</param>
431 /// <returns>true if the table exists and is persistent in the database; false otherwise</returns>
432 /// <exception cref="ArgumentException">the table is unknown</exception>
433 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
434 /// <remarks><p>
435 /// To check whether a table exists regardless of persistence,
436 /// use <see cref="TableCollection.Contains"/>.
437 /// </p><p>
438 /// Win32 MSI API:
439 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseistablepersistent.asp">MsiDatabaseIsTablePersistent</a>
440 /// </p></remarks>
441 public bool IsTablePersistent(string table)
442 {
443 if (String.IsNullOrEmpty(table))
444 {
445 throw new ArgumentNullException("table");
446 }
447 uint ret = RemotableNativeMethods.MsiDatabaseIsTablePersistent((int) this.Handle, table);
448 if (ret == 3) // MSICONDITION_ERROR
449 {
450 throw new InstallerException();
451 }
452 return ret == 1;
453 }
454
455 /// <summary>
456 /// Checks whether a table contains a persistent column with a given name.
457 /// </summary>
458 /// <param name="table">The table to the checked</param>
459 /// <param name="column">The name of the column to be checked</param>
460 /// <returns>true if the column exists in the table; false if the column is temporary or does not exist.</returns>
461 /// <exception cref="InstallerException">the View could not be executed</exception>
462 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
463 /// <remarks><p>
464 /// To check whether a column exists regardless of persistence,
465 /// use <see cref="ColumnCollection.Contains"/>.
466 /// </p></remarks>
467 public bool IsColumnPersistent(string table, string column)
468 {
469 if (String.IsNullOrEmpty(table))
470 {
471 throw new ArgumentNullException("table");
472 }
473 if (String.IsNullOrEmpty(column))
474 {
475 throw new ArgumentNullException("column");
476 }
477 using (View view = this.OpenView(
478 "SELECT `Number` FROM `_Columns` WHERE `Table` = '{0}' AND `Name` = '{1}'", table, column))
479 {
480 view.Execute();
481 using (Record rec = view.Fetch())
482 {
483 return (rec != null);
484 }
485 }
486 }
487
488 /// <summary>
489 /// Gets the count of all rows in the table.
490 /// </summary>
491 /// <param name="table">Name of the table whose rows are to be counted</param>
492 /// <returns>The count of all rows in the table</returns>
493 /// <exception cref="InstallerException">the View could not be executed</exception>
494 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
495 public int CountRows(string table)
496 {
497 return this.CountRows(table, null);
498 }
499
500 /// <summary>
501 /// Gets the count of all rows in the table that satisfy a given condition.
502 /// </summary>
503 /// <param name="table">Name of the table whose rows are to be counted</param>
504 /// <param name="where">Conditional expression, such as could be placed on the end of a SQL WHERE clause</param>
505 /// <returns>The count of all rows in the table satisfying the condition</returns>
506 /// <exception cref="BadQuerySyntaxException">the SQL WHERE syntax is invalid</exception>
507 /// <exception cref="InstallerException">the View could not be executed</exception>
508 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
509 public int CountRows(string table, string where)
510 {
511 if (String.IsNullOrEmpty(table))
512 {
513 throw new ArgumentNullException("table");
514 }
515
516 // to support temporary tables like _Streams, run the query even if the table isn't persistent
517 TableInfo tableInfo = this.Tables[table];
518 string primaryKeys = tableInfo == null ? "*" : String.Concat("`", tableInfo.PrimaryKeys[0], "`");
519 int count;
520
521 try
522 {
523 using (View view = this.OpenView(
524 "SELECT {0} FROM `{1}`{2}",
525 primaryKeys,
526 table,
527 (where != null && where.Length != 0 ? " WHERE " + where : "")))
528 {
529 view.Execute();
530 for (count = 0; ; count++)
531 {
532 // Avoid creating unnecessary Record objects by not calling View.Fetch().
533 int recordHandle;
534 uint ret = RemotableNativeMethods.MsiViewFetch((int)view.Handle, out recordHandle);
535 if (ret == (uint)NativeMethods.Error.NO_MORE_ITEMS)
536 {
537 break;
538 }
539
540 if (ret != 0)
541 {
542 throw InstallerException.ExceptionFromReturnCode(ret);
543 }
544
545 RemotableNativeMethods.MsiCloseHandle(recordHandle);
546 }
547 }
548 }
549 catch (BadQuerySyntaxException)
550 {
551 // table was missing
552 count = 0;
553 }
554
555 return count;
556 }
557
558 /// <summary>
559 /// Finalizes the persistent form of the database. All persistent data is written
560 /// to the writeable database, and no temporary columns or rows are written.
561 /// </summary>
562 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
563 /// <remarks><p>
564 /// For a database open in <see cref="DatabaseOpenMode.ReadOnly"/> mode, this method has no effect.
565 /// </p><p>
566 /// For a database open in <see cref="DatabaseOpenMode.CreateDirect" /> or <see cref="DatabaseOpenMode.Direct" />
567 /// mode, it is not necessary to call this method because the database will be automatically committed
568 /// when it is closed. However this method may be called at any time to persist the current state of tables
569 /// loaded into memory.
570 /// </p><p>
571 /// For a database open in <see cref="DatabaseOpenMode.Create" /> or <see cref="DatabaseOpenMode.Transact" />
572 /// mode, no changes will be persisted until this method is called. If the database object is closed without
573 /// calling this method, the database file remains unmodified.
574 /// </p><p>
575 /// Win32 MSI API:
576 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabasecommit.asp">MsiDatabaseCommit</a>
577 /// </p></remarks>
578 public void Commit()
579 {
580 if (this.summaryInfo != null && !this.summaryInfo.IsClosed)
581 {
582 this.summaryInfo.Persist();
583 this.summaryInfo.Close();
584 this.summaryInfo = null;
585 }
586 uint ret = NativeMethods.MsiDatabaseCommit((int) this.Handle);
587 if (ret != 0)
588 {
589 throw InstallerException.ExceptionFromReturnCode(ret);
590 }
591 }
592
593 /// <summary>
594 /// Copies the structure and data from a specified table to a text archive file.
595 /// </summary>
596 /// <param name="table">Name of the table to be exported</param>
597 /// <param name="exportFilePath">Path to the file to be created</param>
598 /// <exception cref="FileNotFoundException">the file path is invalid</exception>
599 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
600 /// <remarks><p>
601 /// Win32 MSI API:
602 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseexport.asp">MsiDatabaseExport</a>
603 /// </p></remarks>
604 public void Export(string table, string exportFilePath)
605 {
606 if (table == null)
607 {
608 throw new ArgumentNullException("table");
609 }
610
611 FileInfo file = new FileInfo(exportFilePath);
612 uint ret = NativeMethods.MsiDatabaseExport((int) this.Handle, table, file.DirectoryName, file.Name);
613 if (ret != 0)
614 {
615 if (ret == (uint) NativeMethods.Error.BAD_PATHNAME)
616 {
617 throw new FileNotFoundException(null, exportFilePath);
618 }
619 else
620 {
621 throw InstallerException.ExceptionFromReturnCode(ret);
622 }
623 }
624 }
625
626 /// <summary>
627 /// Imports a database table from a text archive file, dropping any existing table.
628 /// </summary>
629 /// <param name="importFilePath">Path to the file to be imported.
630 /// The table name is specified within the file.</param>
631 /// <exception cref="FileNotFoundException">the file path is invalid</exception>
632 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
633 /// <remarks><p>
634 /// Win32 MSI API:
635 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseimport.asp">MsiDatabaseImport</a>
636 /// </p></remarks>
637 public void Import(string importFilePath)
638 {
639 if (String.IsNullOrEmpty(importFilePath))
640 {
641 throw new ArgumentNullException("importFilePath");
642 }
643
644 FileInfo file = new FileInfo(importFilePath);
645 uint ret = NativeMethods.MsiDatabaseImport((int) this.Handle, file.DirectoryName, file.Name);
646 if (ret != 0)
647 {
648 if (ret == (uint) NativeMethods.Error.BAD_PATHNAME)
649 {
650 throw new FileNotFoundException(null, importFilePath);
651 }
652 else
653 {
654 throw InstallerException.ExceptionFromReturnCode(ret);
655 }
656 }
657 }
658
659 /// <summary>
660 /// Exports all database tables, streams, and summary information to archive files.
661 /// </summary>
662 /// <param name="directoryPath">Path to the directory where archive files will be created</param>
663 /// <exception cref="FileNotFoundException">the directory path is invalid</exception>
664 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
665 /// <remarks><p>
666 /// The directory will be created if it does not already exist.
667 /// </p><p>
668 /// Win32 MSI API:
669 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseexport.asp">MsiDatabaseExport</a>
670 /// </p></remarks>
671 public void ExportAll(string directoryPath)
672 {
673 if (String.IsNullOrEmpty(directoryPath))
674 {
675 throw new ArgumentNullException("directoryPath");
676 }
677
678 if (!Directory.Exists(directoryPath))
679 {
680 Directory.CreateDirectory(directoryPath);
681 }
682
683 this.Export("_SummaryInformation", Path.Combine(directoryPath, "_SummaryInformation.idt"));
684
685 using (View view = this.OpenView("SELECT `Name` FROM `_Tables`"))
686 {
687 view.Execute();
688
689 foreach (Record rec in view) using (rec)
690 {
691 string table = (string) rec[1];
692
693 this.Export(table, Path.Combine(directoryPath, table + ".idt"));
694 }
695 }
696
697 if (!Directory.Exists(Path.Combine(directoryPath, "_Streams")))
698 {
699 Directory.CreateDirectory(Path.Combine(directoryPath, "_Streams"));
700 }
701
702 using (View view = this.OpenView("SELECT `Name`, `Data` FROM `_Streams`"))
703 {
704 view.Execute();
705
706 foreach (Record rec in view) using (rec)
707 {
708 string stream = (string) rec[1];
709 if (stream.EndsWith("SummaryInformation", StringComparison.Ordinal)) continue;
710
711 int i = stream.IndexOf('.');
712 if (i >= 0)
713 {
714 if (File.Exists(Path.Combine(
715 directoryPath,
716 Path.Combine(stream.Substring(0, i), stream.Substring(i + 1) + ".ibd"))))
717 {
718 continue;
719 }
720 }
721 rec.GetStream(2, Path.Combine(directoryPath, Path.Combine("_Streams", stream)));
722 }
723 }
724 }
725
726 /// <summary>
727 /// Imports all database tables, streams, and summary information from archive files.
728 /// </summary>
729 /// <param name="directoryPath">Path to the directory from which archive files will be imported</param>
730 /// <exception cref="FileNotFoundException">the directory path is invalid</exception>
731 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
732 /// <remarks><p>
733 /// Win32 MSI API:
734 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseimport.asp">MsiDatabaseImport</a>
735 /// </p></remarks>
736 public void ImportAll(string directoryPath)
737 {
738 if (String.IsNullOrEmpty(directoryPath))
739 {
740 throw new ArgumentNullException("directoryPath");
741 }
742
743 if (File.Exists(Path.Combine(directoryPath, "_SummaryInformation.idt")))
744 {
745 this.Import(Path.Combine(directoryPath, "_SummaryInformation.idt"));
746 }
747
748 string[] idtFiles = Directory.GetFiles(directoryPath, "*.idt");
749 foreach (string file in idtFiles)
750 {
751 if (Path.GetFileName(file) != "_SummaryInformation.idt")
752 {
753 this.Import(file);
754 }
755 }
756
757 if (Directory.Exists(Path.Combine(directoryPath, "_Streams")))
758 {
759 View view = this.OpenView("SELECT `Name`, `Data` FROM `_Streams`");
760 Record rec = null;
761 try
762 {
763 view.Execute();
764 string[] streamFiles = Directory.GetFiles(Path.Combine(directoryPath, "_Streams"));
765 foreach (string file in streamFiles)
766 {
767 rec = this.CreateRecord(2);
768 rec[1] = Path.GetFileName(file);
769 rec.SetStream(2, file);
770 view.Insert(rec);
771 rec.Close();
772 rec = null;
773 }
774 }
775 finally
776 {
777 if (rec != null) rec.Close();
778 view.Close();
779 }
780 }
781 }
782
783 /// <summary>
784 /// Creates a new record object with the requested number of fields.
785 /// </summary>
786 /// <param name="fieldCount">Required number of fields, which may be 0.
787 /// The maximum number of fields in a record is limited to 65535.</param>
788 /// <returns>A new record object that can be used with the database.</returns>
789 /// <remarks><p>
790 /// This method is equivalent to directly calling the <see cref="Record" />
791 /// constructor in all cases outside of a custom action context. When in a
792 /// custom action session, this method allows creation of a record that can
793 /// work with a database other than the session database.
794 /// </p><p>
795 /// The Record object should be <see cref="InstallerHandle.Close"/>d after use.
796 /// It is best that the handle be closed manually as soon as it is no longer
797 /// needed, as leaving lots of unused handles open can degrade performance.
798 /// </p><p>
799 /// Win32 MSI API:
800 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msicreaterecord.asp">MsiCreateRecord</a>
801 /// </p></remarks>
802 public Record CreateRecord(int fieldCount)
803 {
804 int hRecord = RemotableNativeMethods.MsiCreateRecord((uint) fieldCount, (int) this.Handle);
805 return new Record((IntPtr) hRecord, true, (View) null);
806 }
807
808 /// <summary>
809 /// Returns the file path of this database, or the handle value if a file path was not specified.
810 /// </summary>
811 public override string ToString()
812 {
813 if (this.FilePath != null)
814 {
815 return this.FilePath;
816 }
817 else
818 {
819 return "#" + ((int) this.Handle).ToString(CultureInfo.InvariantCulture);
820 }
821 }
822
823 /// <summary>
824 /// Closes the database handle. After closing a handle, further method calls may throw <see cref="InvalidHandleException"/>.
825 /// </summary>
826 /// <param name="disposing">If true, the method has been called directly or
827 /// indirectly by a user's code, so managed and unmanaged resources will be
828 /// disposed. If false, only unmanaged resources will be disposed.</param>
829 protected override void Dispose(bool disposing)
830 {
831 if (!this.IsClosed &&
832 (this.OpenMode == DatabaseOpenMode.CreateDirect ||
833 this.OpenMode == DatabaseOpenMode.Direct))
834 {
835 // Always commit a direct-opened database before closing.
836 // This avoids unexpected corruption of the database.
837 this.Commit();
838 }
839
840 base.Dispose(disposing);
841
842 if (disposing)
843 {
844 if (this.summaryInfo != null)
845 {
846 this.summaryInfo.Close();
847 this.summaryInfo = null;
848 }
849
850 if (this.deleteOnClose != null)
851 {
852 foreach (string path in this.deleteOnClose)
853 {
854 try
855 {
856 if (Directory.Exists(path))
857 {
858 Directory.Delete(path, true);
859 }
860 else
861 {
862 if (File.Exists(path)) File.Delete(path);
863 }
864 }
865 catch (IOException)
866 {
867 }
868 catch (UnauthorizedAccessException)
869 {
870 }
871 }
872 this.deleteOnClose = null;
873 }
874 }
875 }
876
877 private static int Open(string filePath, string outputPath)
878 {
879 if (String.IsNullOrEmpty(filePath))
880 {
881 throw new ArgumentNullException("filePath");
882 }
883
884 if (String.IsNullOrEmpty(outputPath))
885 {
886 throw new ArgumentNullException("outputPath");
887 }
888
889 int hDb;
890 uint ret = NativeMethods.MsiOpenDatabase(filePath, outputPath, out hDb);
891 if (ret != 0)
892 {
893 throw InstallerException.ExceptionFromReturnCode(ret);
894 }
895 return hDb;
896 }
897
898 private static int Open(string filePath, DatabaseOpenMode mode)
899 {
900 if (String.IsNullOrEmpty(filePath))
901 {
902 throw new ArgumentNullException("filePath");
903 }
904
905 if (Path.GetExtension(filePath).Equals(".msp", StringComparison.Ordinal))
906 {
907 const int DATABASEOPENMODE_PATCH = 32;
908 int patchMode = (int) mode | DATABASEOPENMODE_PATCH;
909 mode = (DatabaseOpenMode) patchMode;
910 }
911
912 int hDb;
913 uint ret = NativeMethods.MsiOpenDatabase(filePath, (IntPtr) mode, out hDb);
914 if (ret != 0)
915 {
916 throw InstallerException.ExceptionFromReturnCode(
917 ret,
918 String.Format(CultureInfo.InvariantCulture, "Database=\"{0}\"", filePath));
919 }
920 return hDb;
921 }
922
923 /// <summary>
924 /// Returns the value of the specified property.
925 /// </summary>
926 /// <param name="property">Name of the property to retrieve.</param>
927 public string ExecutePropertyQuery(string property)
928 {
929 IList<string> values = this.ExecuteStringQuery("SELECT `Value` FROM `Property` WHERE `Property` = '{0}'", property);
930 return (values.Count > 0 ? values[0] : null);
931 }
932 }
933}