// Copyright (c) 2012, Outercurve Foundation. // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, // are permitted provided that the following conditions are met: // // - Redistributions of source code must retain the above copyright notice, this // list of conditions and the following disclaimer. // // - Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // // - Neither the name of the Outercurve Foundation nor the names of its // contributors may be used to endorse or promote products derived from this // software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. using System; using System.IO; using System.Data; using System.Text; using System.Collections.Generic; using System.Threading; using System.Xml; using System.Security.Cryptography; using System.Security.Cryptography.Xml; using WebsitePanel.Providers; using OS = WebsitePanel.Providers.OS; using WebsitePanel.Ecommerce.EnterpriseServer; namespace WebsitePanel.EnterpriseServer { public class BackupController { public const string BACKUP_CATALOG_FILE_NAME = "BackupCatalog.xml"; private const int FILE_BUFFER_LENGTH = 5000000; // ~5MB private const string RSA_SETTINGS_KEY = "RSA_KEY"; public static int Backup(bool async, string taskId, int userId, int packageId, int serviceId, int serverId, string backupFileName, int storePackageId, string storePackageFolder, string storeServerFolder, bool deleteTempBackup) { // check demo account int accountCheck = SecurityContext.CheckAccount(DemandAccount.NotDemo | DemandAccount.IsActive); if (accountCheck < 0) return accountCheck; // check admin account accountCheck = SecurityContext.CheckAccount(DemandAccount.IsAdmin); if ((serviceId > 0 || serverId > 0) && accountCheck < 0) return accountCheck; if (accountCheck < 0) deleteTempBackup = true; // check if backup temp folder is available string tempFolder = GetTempBackupFolder(); if (!FolderWriteAccessible(tempFolder)) return BusinessErrorCodes.ERROR_BACKUP_TEMP_FOLDER_UNAVAILABLE; // check destination folder if required if (!String.IsNullOrEmpty(storePackageFolder) && !RemoteServerFolderWriteAccessible(storePackageId, storePackageFolder)) return BusinessErrorCodes.ERROR_BACKUP_DEST_FOLDER_UNAVAILABLE; // check server folder if required if (!String.IsNullOrEmpty(storeServerFolder) && !FolderWriteAccessible(storeServerFolder)) return BusinessErrorCodes.ERROR_BACKUP_SERVER_FOLDER_UNAVAILABLE; // check/reset delete flag accountCheck = SecurityContext.CheckAccount(DemandAccount.IsAdmin); if (accountCheck < 0 && !deleteTempBackup) deleteTempBackup = true; if (async) { BackupAsyncWorker worker = new BackupAsyncWorker(); worker.threadUserId = SecurityContext.User.UserId; worker.taskId = taskId; worker.userId = userId; worker.packageId = packageId; worker.serviceId = serviceId; worker.serverId = serverId; worker.backupFileName = backupFileName; worker.storePackageId = storePackageId; worker.storePackageFolder = storePackageFolder; worker.storeServerFolder = storeServerFolder; worker.deleteTempBackup = deleteTempBackup; // run worker.BackupAsync(); return 0; } else { return BackupInternal(taskId, userId, packageId, serviceId, serverId, backupFileName, storePackageId, storePackageFolder, storeServerFolder, deleteTempBackup); } } public static int BackupInternal(string taskId, int userId, int packageId, int serviceId, int serverId, string backupFileName, int storePackageId, string storePackageFolder, string storeServerFolder, bool deleteTempBackup) { try { TaskManager.StartTask(taskId, "BACKUP", "BACKUP", backupFileName, SecurityContext.User.UserId); // get the list of items to backup TaskManager.Write("Calculate items to backup"); List items = GetBackupItems(userId, packageId, serviceId, serverId); if (items.Count == 0) return 0; // group items by item types Dictionary> groupedItems = new Dictionary>(); // sort by groups foreach (ServiceProviderItem item in items) { // add to group if (!groupedItems.ContainsKey(item.TypeId)) groupedItems[item.TypeId] = new List(); groupedItems[item.TypeId].Add(item); } // temp backup folder string tempFolder = GetTempBackupFolder(); // create backup catalog file StringWriter sw = new StringWriter(); XmlTextWriter writer = new XmlTextWriter(sw); // write backup file header writer.WriteProcessingInstruction("xml", "version=\"1.0\" encoding=\"utf-8\""); writer.WriteStartElement("Backup"); writer.WriteStartElement("Info"); writer.WriteElementString("Name", backupFileName); writer.WriteElementString("Created", DateTime.Now.ToString("r")); writer.WriteElementString("User", GetLoggedUsername()); writer.WriteEndElement(); // Info // determine the number of items to backup int totalItems = 0; foreach (int itemTypeId in groupedItems.Keys) { // load item type ServiceProviderItemType itemType = PackageController.GetServiceItemType(itemTypeId); if (!itemType.Backupable) continue; totalItems += groupedItems[itemTypeId].Count; } TaskManager.IndicatorMaximum = totalItems + 2; TaskManager.IndicatorCurrent = 0; // backup grouped items writer.WriteStartElement("Items"); foreach (int itemTypeId in groupedItems.Keys) { // load item type ServiceProviderItemType itemType = PackageController.GetServiceItemType(itemTypeId); if (!itemType.Backupable) continue; // load group ResourceGroupInfo group = ServerController.GetResourceGroup(itemType.GroupId); // instantiate controller IBackupController controller = null; try { if (group.GroupController != null) controller = Activator.CreateInstance(Type.GetType(group.GroupController)) as IBackupController; if (controller != null) { // backup items foreach (ServiceProviderItem item in groupedItems[itemTypeId]) { TaskManager.Write(String.Format("Backup {0} of {1} - {2} '{3}'", TaskManager.IndicatorCurrent + 1, totalItems, itemType.DisplayName, item.Name)); try { int backupResult = BackupItem(tempFolder, writer, item, group, controller); } catch (Exception ex) { TaskManager.WriteError(ex, "Can't backup item"); } // increment progress TaskManager.IndicatorCurrent += 1; } } } catch (Exception ex) { TaskManager.WriteError(ex); } } writer.WriteEndElement(); // Items // close catalog writer writer.WriteEndElement(); // Backup writer.Close(); // convert to Xml document XmlDocument doc = new XmlDocument(); doc.LoadXml(sw.ToString()); // sign XML document //SignXmlDocument(doc); // save signed doc to file try { doc.Save(Path.Combine(tempFolder, BACKUP_CATALOG_FILE_NAME)); } catch (Exception ex) { TaskManager.WriteError(ex, "Can't save backup catalog file: " + Path.Combine(tempFolder, BACKUP_CATALOG_FILE_NAME)); return 0; } TaskManager.Write("Packaging backup..."); // compress backup files string[] zipFiles = Directory.GetFiles(tempFolder); string[] zipFileNames = new string[zipFiles.Length]; for (int i = 0; i < zipFiles.Length; i++) zipFileNames[i] = Path.GetFileName(zipFiles[i]); string backupFileNamePath = Path.Combine(tempFolder, backupFileName); try { FileUtils.ZipFiles(backupFileNamePath, tempFolder, zipFileNames); // delete packed files foreach (string zipFile in zipFiles) File.Delete(zipFile); } catch (Exception ex) { TaskManager.WriteError(ex, "Can't zip backed up files"); return 0; } TaskManager.IndicatorCurrent += 1; TaskManager.Write("Copying backup..."); // move/copy backup file if (!String.IsNullOrEmpty(storeServerFolder)) { // copy to local folder or UNC try { string destFile = Path.Combine(storeServerFolder, backupFileName); File.Copy(backupFileNamePath, destFile, true); } catch (Exception ex) { TaskManager.WriteError(ex, "Can't copy backup to destination location"); return 0; } } else if (storePackageId > 0) { try { // copy to space folder int osServiceId = PackageController.GetPackageServiceId(storePackageId, ResourceGroups.Os); if (osServiceId > 0) { OS.OperatingSystem os = new OS.OperatingSystem(); ServiceProviderProxy.Init(os, osServiceId); string remoteBackupPath = FilesController.GetFullPackagePath(storePackageId, Path.Combine(storePackageFolder, backupFileName)); FileStream stream = new FileStream(backupFileNamePath, FileMode.Open, FileAccess.Read); byte[] buffer = new byte[FILE_BUFFER_LENGTH]; int readBytes = 0; do { // read package file readBytes = stream.Read(buffer, 0, FILE_BUFFER_LENGTH); if (readBytes < FILE_BUFFER_LENGTH) // resize buffer Array.Resize(ref buffer, readBytes); // write remote backup file os.AppendFileBinaryContent(remoteBackupPath, buffer); } while (readBytes == FILE_BUFFER_LENGTH); stream.Close(); } } catch (Exception ex) { TaskManager.WriteError(ex, "Can't copy backup to destination hosting space"); return 0; } } TaskManager.IndicatorCurrent += 1; // delete backup file if required if (deleteTempBackup) { try { // delete backup folder and all its contents Directory.Delete(tempFolder, true); } catch (Exception ex) { TaskManager.WriteError(ex, "Can't delete temporary backup folder"); return 0; } } BackgroundTask topTask = TaskManager.TopTask; topTask.IndicatorCurrent = topTask.IndicatorMaximum; TaskController.UpdateTask(topTask); } catch (Exception ex) { TaskManager.WriteError(ex); } finally { TaskManager.CompleteTask(); } return 0; } public static List GetBackupItems(int userId, int packageId, int serviceId, int serverId) { List items = new List(); if (packageId > 0) { items.AddRange(PackageController.GetPackageItems(packageId)); } else if (serviceId > 0) { items.AddRange(PackageController.GetServiceItems(serviceId)); } else if (serverId > 0) { // get server services List services = ServerController.GetServicesByServerId(serverId); foreach (ServiceInfo service in services) items.AddRange(PackageController.GetServiceItems(service.ServiceId)); } else if (userId > 0) { List packages = new List(); // get own spaces packages.AddRange(PackageController.GetMyPackages(userId)); // get user spaces packages.AddRange(PackageController.GetPackages(userId)); // build collection foreach (PackageInfo package in packages) items.AddRange(PackageController.GetPackageItems(package.PackageId)); } return items; } public static KeyValueBunch GetBackupContentSummary(int userId, int packageId, int serviceId, int serverId) { Dictionary> summary = new Dictionary>(); // Get backup items List items = GetBackupItems(userId, packageId, serviceId, serverId); // Prepare filter for in-loop sort ServiceProviderItemType[] itemTypes = PackageController.GetServiceItemTypes().ToArray(); // Group service items by type id foreach (ServiceProviderItem si in items) { ServiceProviderItemType itemType = Array.Find(itemTypes, x => x.ItemTypeId == si.TypeId && x.Backupable); // Sort out item types without backup capabilities if (itemType != null) { // Mimic a grouping sort if (!summary.ContainsKey(itemType.DisplayName)) summary.Add(itemType.DisplayName, new List()); // summary[itemType.DisplayName].Add(si.Name); } } // KeyValueBunch result = new KeyValueBunch(); // Convert grouped data into serializable format foreach (string groupName in summary.Keys) result[groupName] = String.Join(",", summary[groupName].ToArray()); // return result; } public static int Restore(bool async, string taskId, int userId, int packageId, int serviceId, int serverId, int storePackageId, string storePackageBackupPath, string storeServerBackupPath) { // check demo account int accountCheck = SecurityContext.CheckAccount(DemandAccount.NotDemo); if (accountCheck < 0) return accountCheck; // check admin account accountCheck = SecurityContext.CheckAccount(DemandAccount.IsAdmin); if ((serviceId > 0 || serverId > 0) && accountCheck < 0) return accountCheck; // check if backup temp folder is available string tempFolder = GetTempBackupFolder(); if (!FolderWriteAccessible(tempFolder)) return BusinessErrorCodes.ERROR_BACKUP_TEMP_FOLDER_UNAVAILABLE; // check server source path if required if (!String.IsNullOrEmpty(storeServerBackupPath)) { try { if (!File.Exists(storeServerBackupPath)) return BusinessErrorCodes.ERROR_RESTORE_BACKUP_SOURCE_NOT_FOUND; } catch { return BusinessErrorCodes.ERROR_RESTORE_BACKUP_SOURCE_UNAVAILABLE; } } if (async) { BackupAsyncWorker worker = new BackupAsyncWorker(); worker.threadUserId = SecurityContext.User.UserId; worker.taskId = taskId; worker.userId = userId; worker.packageId = packageId; worker.serviceId = serviceId; worker.serverId = serverId; worker.storePackageId = storePackageId; worker.storePackageBackupPath = storePackageBackupPath; worker.storeServerBackupPath = storeServerBackupPath; // run worker.RestoreAsync(); return 0; } else { return RestoreInternal(taskId, userId, packageId, serviceId, serverId, storePackageId, storePackageBackupPath, storeServerBackupPath); } } public static int RestoreInternal(string taskId, int userId, int packageId, int serviceId, int serverId, int storePackageId, string storePackageBackupPath, string storeServerBackupPath) { try { // copy backup from remote or local server string backupFileName = (storePackageId > 0) ? Path.GetFileName(storePackageBackupPath) : Path.GetFileName(storeServerBackupPath); TaskManager.StartTask(taskId, "BACKUP", "RESTORE", backupFileName, SecurityContext.User.UserId); // create temp folder string tempFolder = GetTempBackupFolder(); string backupFileNamePath = Path.Combine(tempFolder, backupFileName); if (storePackageId > 0) { try { int osServiceId = PackageController.GetPackageServiceId(storePackageId, ResourceGroups.Os); if (osServiceId > 0) { OS.OperatingSystem os = new OS.OperatingSystem(); ServiceProviderProxy.Init(os, osServiceId); string remoteBackupPath = FilesController.GetFullPackagePath(storePackageId, storePackageBackupPath); FileStream stream = new FileStream(backupFileNamePath, FileMode.Create, FileAccess.Write); byte[] buffer = new byte[FILE_BUFFER_LENGTH]; int offset = 0; do { // read remote content buffer = os.GetFileBinaryChunk(remoteBackupPath, offset, FILE_BUFFER_LENGTH); // write remote content stream.Write(buffer, 0, buffer.Length); offset += FILE_BUFFER_LENGTH; } while (buffer.Length == FILE_BUFFER_LENGTH); stream.Close(); } } catch (Exception ex) { TaskManager.WriteError(ex, "Can't copy source backup set"); return 0; } } else { backupFileNamePath = storeServerBackupPath; } try { // unpack archive FileUtils.UnzipFiles(backupFileNamePath, tempFolder); } catch (Exception ex) { TaskManager.WriteError(ex, "Can't unzip backup set"); return 0; } // load backup catalog XmlDocument doc = new XmlDocument(); try { doc.Load(Path.Combine(tempFolder, BACKUP_CATALOG_FILE_NAME)); } catch (Exception ex) { TaskManager.WriteError(ex, "Can't find/open backup catalog file"); return 0; } // validate XML document //if (!ValidateXmlDocument(doc)) //{ // TaskManager.WriteError("Corrupted or altered backup catalog file has been read"); // return 0; //} // get the list of items to restore string condition = ""; if (userId > 0) { // get user spaces List packages = new List(); packages.AddRange(PackageController.GetMyPackages(userId)); packages.AddRange(PackageController.GetPackages(userId)); List parts = new List(); foreach (PackageInfo package in packages) parts.Add("@packageId = " + package.PackageId.ToString()); condition = "[" + String.Join(" or ", parts.ToArray()) + "]"; } else if (packageId > 0) { condition = "[@packageId = " + packageId + "]"; } else if (serviceId > 0) { condition = "[@serviceId = " + serviceId + "]"; } else if (serverId > 0) { // get server services List services = ServerController.GetServicesByServerId(serverId); List parts = new List(); foreach (ServiceInfo service in services) parts.Add("@serviceId = " + service.ServiceId.ToString()); condition = "[" + String.Join(" or ", parts.ToArray()) + "]"; } XmlNodeList itemNodes = doc.SelectNodes("Backup/Items/Item" + condition); TaskManager.IndicatorMaximum = itemNodes.Count; TaskManager.IndicatorCurrent = 0; // group items by item types Dictionary> groupedItems = new Dictionary>(); // sort by groups foreach (XmlNode itemNode in itemNodes) { int itemTypeId = Utils.ParseInt(itemNode.Attributes["itemTypeId"].Value, 0); // add to group if (!groupedItems.ContainsKey(itemTypeId)) groupedItems[itemTypeId] = new List(); groupedItems[itemTypeId].Add(itemNode); } // restore grouped items foreach (int itemTypeId in groupedItems.Keys) { // load item type ServiceProviderItemType itemTypeInfo = PackageController.GetServiceItemType(itemTypeId); if (!itemTypeInfo.Backupable) continue; Type itemType = Type.GetType(itemTypeInfo.TypeName); // load group ResourceGroupInfo group = ServerController.GetResourceGroup(itemTypeInfo.GroupId); // instantiate controller IBackupController controller = null; try { controller = Activator.CreateInstance(Type.GetType(group.GroupController)) as IBackupController; if (controller != null) { // backup items foreach (XmlNode itemNode in groupedItems[itemTypeId]) { int itemId = Utils.ParseInt(itemNode.Attributes["itemId"].Value, 0); string itemName = itemNode.Attributes["itemName"].Value; int itemPackageId = Utils.ParseInt(itemNode.Attributes["packageId"].Value, 0); int itemServiceId = Utils.ParseInt(itemNode.Attributes["serviceId"].Value, 0); TaskManager.Write(String.Format("Restore {0} '{1}'", itemTypeInfo.DisplayName, itemName)); try { int restoreResult = controller.RestoreItem(tempFolder, itemNode, itemId, itemType, itemName, itemPackageId, itemServiceId, group); } catch (Exception ex) { TaskManager.WriteError(ex, "Can't restore item"); } TaskManager.IndicatorCurrent++; } } } catch (Exception ex) { TaskManager.WriteError(ex); } } // delete backup folder and all its contents try { Directory.Delete(tempFolder, true); } catch (Exception ex) { TaskManager.WriteError(ex, "Can't delete temporary backup folder"); return 0; } } catch (Exception ex) { TaskManager.WriteError(ex); } finally { TaskManager.CompleteTask(); } return 0; } public static int BackupItem(string tempFolder, XmlWriter writer, ServiceProviderItem item) { // load item type ServiceProviderItemType itemType = PackageController.GetServiceItemType(item.TypeId); if (!itemType.Backupable) return -1; // load group ResourceGroupInfo group = ServerController.GetResourceGroup(itemType.GroupId); // create controller IBackupController controller = null; try { controller = Activator.CreateInstance(Type.GetType(group.GroupController)) as IBackupController; if (controller != null) { return BackupItem(tempFolder, writer, item, group, controller); } } catch (Exception ex) { TaskManager.WriteError(ex); } return -2; } private static int BackupItem(string tempFolder, XmlWriter writer, ServiceProviderItem item, ResourceGroupInfo group, IBackupController controller) { writer.WriteStartElement("Item"); writer.WriteAttributeString("itemId", item.Id.ToString()); writer.WriteAttributeString("itemTypeId", item.TypeId.ToString()); writer.WriteAttributeString("itemName", item.Name); writer.WriteAttributeString("packageId", item.PackageId.ToString()); writer.WriteAttributeString("serviceId", item.ServiceId.ToString()); try { return controller.BackupItem(tempFolder, writer, item, group); } catch (Exception ex) { TaskManager.WriteError(ex); } finally { writer.WriteEndElement(); // Item } return 0; } public static int RestoreItem() { return 0; } public static void WriteFileElement(XmlWriter writer, string fileName, string filePath, long size) { writer.WriteStartElement("File"); writer.WriteAttributeString("name", fileName); writer.WriteAttributeString("path", filePath); writer.WriteAttributeString("size", size.ToString()); writer.WriteEndElement(); } #region Utility Methods public static bool FolderWriteAccessible(string path) { try { string tempFile = Path.Combine(path, "check"); StreamWriter writer = File.CreateText(tempFile); writer.Close(); File.Delete(tempFile); return true; } catch { return false; } } public static bool RemoteServerFolderWriteAccessible(int packageId, string path) { try { // copy to space folder int osServiceId = PackageController.GetPackageServiceId(packageId, ResourceGroups.Os); if (osServiceId > 0) { OS.OperatingSystem os = new OS.OperatingSystem(); ServiceProviderProxy.Init(os, osServiceId); string remoteServerPathCheck = FilesController.GetFullPackagePath(packageId, Path.Combine(path, "check.txt")); // os.CreateFile(remoteServerPathCheck); os.AppendFileBinaryContent(remoteServerPathCheck, Encoding.UTF8.GetBytes(remoteServerPathCheck)); os.DeleteFile(remoteServerPathCheck); } // return true; } catch { // return false; } } public static void SignXmlDocument(XmlDocument doc) { // Create a SignedXml object. SignedXml signedXml = new SignedXml(doc); // Add the key to the SignedXml document. signedXml.SigningKey = GetUserRSAKey(); // Create a reference to be signed. Reference reference = new Reference(); reference.Uri = ""; // Add an enveloped transformation to the reference. XmlDsigEnvelopedSignatureTransform env = new XmlDsigEnvelopedSignatureTransform(); reference.AddTransform(env); // Add the reference to the SignedXml object. signedXml.AddReference(reference); // Compute the signature. signedXml.ComputeSignature(); // Get the XML representation of the signature and save // it to an XmlElement object. XmlElement xmlDigitalSignature = signedXml.GetXml(); // Append the element to the XML document. doc.DocumentElement.AppendChild(doc.ImportNode(xmlDigitalSignature, true)); } public static bool ValidateXmlDocument(XmlDocument doc) { // Create a new SignedXml object and pass it // the XML document class. SignedXml signedXml = new SignedXml(doc); // Find the "Signature" node and create a new // XmlNodeList object. XmlNodeList nodeList = doc.GetElementsByTagName("Signature"); // Throw an exception if no signature was found. if (nodeList.Count <= 0) { throw new CryptographicException("Verification failed: No Signature was found in the document."); } // This example only supports one signature for // the entire XML document. Throw an exception // if more than one signature was found. if (nodeList.Count >= 2) { throw new CryptographicException("Verification failed: More that one signature was found for the document."); } // Load the first node. signedXml.LoadXml((XmlElement)nodeList[0]); // Check the signature and return the result. return signedXml.CheckSignature(GetUserRSAKey()); } public static RSA GetUserRSAKey() { int userId = SecurityContext.User.UserId; if(SecurityContext.User.IsPeer) userId = SecurityContext.User.OwnerId; UserSettings settings = UserController.GetUserSettings(userId, RSA_SETTINGS_KEY); string keyXml = settings[RSA_SETTINGS_KEY]; RSA rsa = RSA.Create(); if (String.IsNullOrEmpty(keyXml) || settings.UserId != userId) { // generate new key keyXml = rsa.ToXmlString(true); // store to settings settings[RSA_SETTINGS_KEY] = keyXml; settings.UserId = userId; UserController.UpdateUserSettings(settings); } else { rsa.FromXmlString(keyXml); } return rsa; } private static string GetLoggedUsername() { string username = SecurityContext.User.Identity.Name; UserInfo user = UserController.GetUser(SecurityContext.User.UserId); if (user != null) username = user.Username; return username; } public static string GetTempBackupFolder() { string timeStamp = DateTime.Now.Ticks.ToString(); string tempFolder = Path.Combine(ConfigSettings.BackupsPath, GetLoggedUsername() + "_" + timeStamp); // create folder if (!Directory.Exists(tempFolder)) Directory.CreateDirectory(tempFolder); return tempFolder; } #endregion } }