From 162a59ba17af6c1fe16036ae3d1510d5c895b914 Mon Sep 17 00:00:00 2001 From: Justin Clarke Casey Date: Wed, 11 Feb 2009 18:46:51 +0000 Subject: * Refactor inventory archive code to allow direct invocation in order to support future unit tests * Add a file I missed out from the last commit (the build was probably fine without it) --- OpenSim/Region/Application/OpenSimBase.cs | 4 +- .../Archiver/InventoryArchiveReadRequest.cs | 67 +++++--- .../Archiver/InventoryArchiveWriteRequest.cs | 90 +++++++---- .../Inventory/Archiver/InventoryArchiverModule.cs | 169 +++++++++++++++++++++ .../Interfaces/IInventoryArchiverModule.cs | 52 +++++++ 5 files changed, 328 insertions(+), 54 deletions(-) create mode 100644 OpenSim/Region/CoreModules/Avatar/Inventory/Archiver/InventoryArchiverModule.cs create mode 100644 OpenSim/Region/Framework/Interfaces/IInventoryArchiverModule.cs diff --git a/OpenSim/Region/Application/OpenSimBase.cs b/OpenSim/Region/Application/OpenSimBase.cs index 6592a9b..43cb127 100644 --- a/OpenSim/Region/Application/OpenSimBase.cs +++ b/OpenSim/Region/Application/OpenSimBase.cs @@ -164,7 +164,7 @@ namespace OpenSim } /// - /// Performs startup specific to this region server, including initialization of the scene + /// Performs startup specific to the region server, including initialization of the scene /// such as loading configuration from disk. /// protected override void StartupSpecific() @@ -175,7 +175,7 @@ namespace OpenSim LibraryRootFolder libraryRootFolder = new LibraryRootFolder(m_configSettings.LibrariesXMLFile); - // StandAlone mode? is determined by !startupConfig.GetBoolean("gridmode", false) + // Standalone mode is determined by !startupConfig.GetBoolean("gridmode", false) if (m_configSettings.Standalone) { InitialiseStandaloneServices(libraryRootFolder); diff --git a/OpenSim/Region/CoreModules/Avatar/Inventory/Archiver/InventoryArchiveReadRequest.cs b/OpenSim/Region/CoreModules/Avatar/Inventory/Archiver/InventoryArchiveReadRequest.cs index 704296c..7e57275 100644 --- a/OpenSim/Region/CoreModules/Avatar/Inventory/Archiver/InventoryArchiveReadRequest.cs +++ b/OpenSim/Region/CoreModules/Avatar/Inventory/Archiver/InventoryArchiveReadRequest.cs @@ -50,13 +50,37 @@ namespace OpenSim.Region.CoreModules.Avatar.Inventory.Archiver protected TarArchiveReader archive; private static System.Text.ASCIIEncoding m_asciiEncoding = new System.Text.ASCIIEncoding(); + private string m_firstName; + private string m_lastName; + private string m_invPath; + + /// + /// The stream from which the inventory archive will be loaded. + /// + private Stream m_loadStream; + CommunicationsManager commsManager; - public InventoryArchiveReadRequest(CommunicationsManager commsManager) + public InventoryArchiveReadRequest( + string firstName, string lastName, string invPath, string loadPath, CommunicationsManager commsManager) + : this( + firstName, + lastName, + invPath, + new GZipStream(new FileStream(loadPath, FileMode.Open), CompressionMode.Decompress), + commsManager) { - //List serialisedObjects = new List(); - this.commsManager = commsManager; } + + public InventoryArchiveReadRequest( + string firstName, string lastName, string invPath, Stream loadStream, CommunicationsManager commsManager) + { + m_firstName = firstName; + m_lastName = lastName; + m_invPath = invPath; + m_loadStream = loadStream; + this.commsManager = commsManager; + } protected InventoryItemBase loadInvItem(string path, string contents) { @@ -137,17 +161,17 @@ namespace OpenSim.Region.CoreModules.Avatar.Inventory.Archiver return item; } - public void execute(string firstName, string lastName, string invPath, string loadPath) + public void Execute() { string filePath = "ERROR"; int successfulAssetRestores = 0; int failedAssetRestores = 0; int successfulItemRestores = 0; - UserProfileData userProfile = commsManager.UserService.GetUserProfile(firstName, lastName); + UserProfileData userProfile = commsManager.UserService.GetUserProfile(m_firstName, m_lastName); if (null == userProfile) { - m_log.ErrorFormat("[CONSOLE]: Failed to find user {0} {1}", firstName, lastName); + m_log.ErrorFormat("[INVENTORY ARCHIVER]: Failed to find user {0} {1}", m_firstName, m_lastName); return; } @@ -155,8 +179,8 @@ namespace OpenSim.Region.CoreModules.Avatar.Inventory.Archiver if (null == userInfo) { m_log.ErrorFormat( - "[CONSOLE]: Failed to find user info for {0} {1} {2}", - firstName, lastName, userProfile.ID); + "[INVENTORY ARCHIVER]: Failed to find user info for {0} {1} {2}", + m_firstName, m_lastName, userProfile.ID); return; } @@ -164,33 +188,32 @@ namespace OpenSim.Region.CoreModules.Avatar.Inventory.Archiver if (!userInfo.HasReceivedInventory) { m_log.ErrorFormat( - "[CONSOLE]: Have not yet received inventory info for user {0} {1} {2}", - firstName, lastName, userProfile.ID); + "[INVENTORY ARCHIVER]: Have not yet received inventory info for user {0} {1} {2}", + m_firstName, m_lastName, userProfile.ID); return; } - InventoryFolderImpl inventoryFolder = userInfo.RootFolder.FindFolderByPath(invPath); + InventoryFolderImpl inventoryFolder = userInfo.RootFolder.FindFolderByPath(m_invPath); if (null == inventoryFolder) { // TODO: Later on, automatically create this folder if it does not exist - m_log.ErrorFormat("[ARCHIVER]: Inventory path {0} does not exist", invPath); + m_log.ErrorFormat("[INVENTORY ARCHIVER]: Inventory path {0} does not exist", m_invPath); return; } - archive - = new TarArchiveReader(new GZipStream( - new FileStream(loadPath, FileMode.Open), CompressionMode.Decompress)); + archive = new TarArchiveReader(m_loadStream); byte[] data; TarArchiveReader.TarEntryType entryType; while ((data = archive.ReadEntry(out filePath, out entryType)) != null) { if (entryType == TarArchiveReader.TarEntryType.TYPE_DIRECTORY) { - m_log.WarnFormat("[ARCHIVER]: Ignoring directory entry {0}", filePath); - } else if (filePath.StartsWith(ArchiveConstants.ASSETS_PATH)) + m_log.WarnFormat("[INVENTORY ARCHIVER]: Ignoring directory entry {0}", filePath); + } + else if (filePath.StartsWith(ArchiveConstants.ASSETS_PATH)) { if (LoadAsset(filePath, data)) successfulAssetRestores++; @@ -219,8 +242,8 @@ namespace OpenSim.Region.CoreModules.Avatar.Inventory.Archiver archive.Close(); - m_log.DebugFormat("[ARCHIVER]: Restored {0} assets", successfulAssetRestores); - m_log.InfoFormat("[ARCHIVER]: Restored {0} items", successfulItemRestores); + m_log.DebugFormat("[INVENTORY ARCHIVER]: Restored {0} assets", successfulAssetRestores); + m_log.InfoFormat("[INVENTORY ARCHIVER]: Restored {0} items", successfulItemRestores); } /// @@ -239,7 +262,7 @@ namespace OpenSim.Region.CoreModules.Avatar.Inventory.Archiver if (i == -1) { m_log.ErrorFormat( - "[ARCHIVER]: Could not find extension information in asset path {0} since it's missing the separator {1}. Skipping", + "[INVENTORY ARCHIVER]: Could not find extension information in asset path {0} since it's missing the separator {1}. Skipping", assetPath, ArchiveConstants.ASSET_EXTENSION_SEPARATOR); return false; @@ -252,7 +275,7 @@ namespace OpenSim.Region.CoreModules.Avatar.Inventory.Archiver { sbyte assetType = ArchiveConstants.EXTENSION_TO_ASSET_TYPE[extension]; - m_log.DebugFormat("[ARCHIVER]: Importing asset {0}, type {1}", uuid, assetType); + m_log.DebugFormat("[INVENTORY ARCHIVER]: Importing asset {0}, type {1}", uuid, assetType); AssetBase asset = new AssetBase(new UUID(uuid), "RandomName"); @@ -266,7 +289,7 @@ namespace OpenSim.Region.CoreModules.Avatar.Inventory.Archiver else { m_log.ErrorFormat( - "[ARCHIVER]: Tried to dearchive data with path {0} with an unknown type extension {1}", + "[INVENTORY ARCHIVER]: Tried to dearchive data with path {0} with an unknown type extension {1}", assetPath, extension); return false; diff --git a/OpenSim/Region/CoreModules/Avatar/Inventory/Archiver/InventoryArchiveWriteRequest.cs b/OpenSim/Region/CoreModules/Avatar/Inventory/Archiver/InventoryArchiveWriteRequest.cs index 90e2fcd..bfa4de9 100644 --- a/OpenSim/Region/CoreModules/Avatar/Inventory/Archiver/InventoryArchiveWriteRequest.cs +++ b/OpenSim/Region/CoreModules/Avatar/Inventory/Archiver/InventoryArchiveWriteRequest.cs @@ -43,29 +43,53 @@ namespace OpenSim.Region.CoreModules.Avatar.Inventory.Archiver { public class InventoryArchiveWriteRequest { - private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); - protected TarArchiveWriter archive; + protected TarArchiveWriter archive = new TarArchiveWriter(); protected CommunicationsManager commsManager; - Dictionary assetUuids; + protected Dictionary assetUuids = new Dictionary(); + + private string m_firstName; + private string m_lastName; + private string m_invPath; /// - /// The path to which the inventory archive will be saved. + /// The stream to which the inventory archive will be saved. /// - private string m_savePath; - - public InventoryArchiveWriteRequest(CommunicationsManager commsManager) + private Stream m_saveStream; + + /// + /// Constructor + /// + public InventoryArchiveWriteRequest( + string firstName, string lastName, string invPath, string savePath, CommunicationsManager commsManager) + : this( + firstName, + lastName, + invPath, + new GZipStream(new FileStream(savePath, FileMode.Create), CompressionMode.Compress), + commsManager) + { + } + + /// + /// Constructor + /// + public InventoryArchiveWriteRequest( + string firstName, string lastName, string invPath, Stream saveStream, CommunicationsManager commsManager) { - archive = new TarArchiveWriter(); + m_firstName = firstName; + m_lastName = lastName; + m_invPath = invPath; + m_saveStream = saveStream; this.commsManager = commsManager; - assetUuids = new Dictionary(); } protected void ReceivedAllAssets(IDictionary assetsFound, ICollection assetsNotFoundUuids) { AssetsArchiver assetsArchiver = new AssetsArchiver(assetsFound); assetsArchiver.Archive(archive); - archive.WriteTar(new GZipStream(new FileStream(m_savePath, FileMode.Create), CompressionMode.Compress)); + archive.WriteTar(m_saveStream); } protected void saveInvItem(InventoryItemBase inventoryItem, string path) @@ -158,21 +182,21 @@ namespace OpenSim.Region.CoreModules.Avatar.Inventory.Archiver } } - public void execute(string firstName, string lastName, string invPath, string savePath) + public void Execute() { - m_savePath = savePath; - - UserProfileData userProfile = commsManager.UserService.GetUserProfile(firstName, lastName); + UserProfileData userProfile = commsManager.UserService.GetUserProfile(m_firstName, m_lastName); if (null == userProfile) { - m_log.ErrorFormat("[CONSOLE]: Failed to find user {0} {1}", firstName, lastName); + m_log.ErrorFormat("[INVENTORY ARCHIVER]: Failed to find user {0} {1}", m_firstName, m_lastName); return; } CachedUserInfo userInfo = commsManager.UserProfileCacheService.GetUserDetails(userProfile.ID); if (null == userInfo) { - m_log.ErrorFormat("[CONSOLE]: Failed to find user info for {0} {1} {2}", firstName, lastName, userProfile.ID); + m_log.ErrorFormat( + "[INVENTORY ARCHIVER]: Failed to find user info for {0} {1} {2}", + m_firstName, m_lastName, userProfile.ID); return; } @@ -184,34 +208,36 @@ namespace OpenSim.Region.CoreModules.Avatar.Inventory.Archiver // Eliminate double slashes and any leading / on the path. This might be better done within InventoryFolderImpl // itself (possibly at a small loss in efficiency). string[] components - = invPath.Split(new string[] { InventoryFolderImpl.PATH_DELIMITER }, StringSplitOptions.RemoveEmptyEntries); - invPath = String.Empty; + = m_invPath.Split(new string[] { InventoryFolderImpl.PATH_DELIMITER }, StringSplitOptions.RemoveEmptyEntries); + m_invPath = String.Empty; foreach (string c in components) { - invPath += c + InventoryFolderImpl.PATH_DELIMITER; + m_invPath += c + InventoryFolderImpl.PATH_DELIMITER; } // Annoyingly Split actually returns the original string if the input string consists only of delimiters // Therefore if we still start with a / after the split, then we need the root folder - if (invPath.Length == 0) + if (m_invPath.Length == 0) { inventoryFolder = userInfo.RootFolder; } else { - invPath = invPath.Remove(invPath.LastIndexOf(InventoryFolderImpl.PATH_DELIMITER)); - inventoryFolder = userInfo.RootFolder.FindFolderByPath(invPath); + m_invPath = m_invPath.Remove(m_invPath.LastIndexOf(InventoryFolderImpl.PATH_DELIMITER)); + inventoryFolder = userInfo.RootFolder.FindFolderByPath(m_invPath); } // The path may point to an item instead if (inventoryFolder == null) { - inventoryItem = userInfo.RootFolder.FindItemByPath(invPath); + inventoryItem = userInfo.RootFolder.FindItemByPath(m_invPath); } } else { - m_log.ErrorFormat("[CONSOLE]: Have not yet received inventory info for user {0} {1} {2}", firstName, lastName, userProfile.ID); + m_log.ErrorFormat( + "[INVENTORY ARCHIVER]: Have not yet received inventory info for user {0} {1} {2}", + m_firstName, m_lastName, userProfile.ID); return; } @@ -219,21 +245,25 @@ namespace OpenSim.Region.CoreModules.Avatar.Inventory.Archiver { if (null == inventoryItem) { - m_log.ErrorFormat("[CONSOLE]: Could not find inventory entry at path {0}", invPath); + m_log.ErrorFormat("[INVENTORY ARCHIVER]: Could not find inventory entry at path {0}", m_invPath); return; } else { - m_log.InfoFormat("[CONSOLE]: Found item {0} {1} at {2}", inventoryItem.Name, inventoryItem.ID, - invPath); + m_log.InfoFormat( + "[INVENTORY ARCHIVER]: Found item {0} {1} at {2}", + inventoryItem.Name, inventoryItem.ID, m_invPath); + //get and export item info - saveInvItem(inventoryItem, invPath); + saveInvItem(inventoryItem, m_invPath); } } else { - m_log.InfoFormat("[CONSOLE]: Found folder {0} {1} at {2}", inventoryFolder.Name, inventoryFolder.ID, - invPath); + m_log.InfoFormat( + "[INVENTORY ARCHIVER]: Found folder {0} {1} at {2}", + inventoryFolder.Name, inventoryFolder.ID, m_invPath); + //recurse through all dirs getting dirs and files saveInvDir(inventoryFolder, ""); } diff --git a/OpenSim/Region/CoreModules/Avatar/Inventory/Archiver/InventoryArchiverModule.cs b/OpenSim/Region/CoreModules/Avatar/Inventory/Archiver/InventoryArchiverModule.cs new file mode 100644 index 0000000..0c489e5 --- /dev/null +++ b/OpenSim/Region/CoreModules/Avatar/Inventory/Archiver/InventoryArchiverModule.cs @@ -0,0 +1,169 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * 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 OpenSim Project 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 DEVELOPERS ``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 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.Collections.Generic; +using System.IO; +using System.Reflection; +using log4net; +using Nini.Config; +using OpenMetaverse; +using OpenSim.Framework.Communications; +using OpenSim.Region.Framework.Interfaces; +using OpenSim.Region.Framework.Scenes; + +namespace OpenSim.Region.CoreModules.Avatar.Inventory.Archiver +{ + /// + /// This module loads and saves OpenSimulator inventory archives + /// + public class InventoryArchiverModule : IRegionModule, IInventoryArchiverModule + { + private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + public string Name { get { return "Inventory Archiver Module"; } } + + public bool IsSharedModule { get { return true; } } + + /// + /// The file to load and save inventory if no filename has been specified + /// + protected const string DEFAULT_INV_BACKUP_FILENAME = "user-inventory_iar.tar.gz"; + + /// + /// All scenes that this module knows about + /// + private Dictionary m_scenes = new Dictionary(); + + /// + /// The comms manager we will use for all comms requests + /// + private CommunicationsManager m_commsManager; + + public void Initialise(Scene scene, IConfigSource source) + { + if (m_scenes.Count == 0) + { + scene.RegisterModuleInterface(this); + m_commsManager = scene.CommsManager; + + scene.AddCommand( + this, "load iar", + "load iar []", + "Load user inventory archive. EXPERIMENTAL, PLEASE DO NOT USE YET", HandleLoadInvConsoleCommand); + + scene.AddCommand( + this, "save iar", + "save iar []", + "Save user inventory archive. EXPERIMENTAL, PLEASE DO NOT USE YET", HandleSaveInvConsoleCommand); + } + + m_scenes[scene.RegionInfo.RegionID] = scene; + } + + public void PostInitialise() + { + } + + public void Close() + { + } + + public void DearchiveInventory(string firstName, string lastName, string invPath, Stream loadStream) + { + if (m_scenes.Count > 0) + { + new InventoryArchiveReadRequest(firstName, lastName, invPath, loadStream, m_commsManager).Execute(); + } + } + + public void ArchiveInventory(string firstName, string lastName, string invPath, Stream saveStream) + { + if (m_scenes.Count > 0) + { + new InventoryArchiveWriteRequest(firstName, lastName, invPath, saveStream, m_commsManager).Execute(); + } + } + + public void DearchiveInventory(string firstName, string lastName, string invPath, string loadPath) + { + if (m_scenes.Count > 0) + { + new InventoryArchiveReadRequest(firstName, lastName, invPath, loadPath, m_commsManager).Execute(); + } + } + + public void ArchiveInventory(string firstName, string lastName, string invPath, string savePath) + { + if (m_scenes.Count > 0) + { + new InventoryArchiveWriteRequest(firstName, lastName, invPath, savePath, m_commsManager).Execute(); + } + } + + /// + /// Load inventory from an inventory file archive + /// + /// + protected void HandleLoadInvConsoleCommand(string module, string[] cmdparams) + { + if (cmdparams.Length < 5) + { + m_log.Error( + "[INVENTORY ARCHIVER]: usage is load iar []"); + return; + } + + string firstName = cmdparams[2]; + string lastName = cmdparams[3]; + string invPath = cmdparams[4]; + string loadPath = (cmdparams.Length > 5 ? cmdparams[5] : DEFAULT_INV_BACKUP_FILENAME); + + DearchiveInventory(firstName, lastName, invPath, loadPath); + } + + /// + /// Save inventory to a file archive + /// + /// + protected void HandleSaveInvConsoleCommand(string module, string[] cmdparams) + { + if (cmdparams.Length < 5) + { + m_log.Error( + "[INVENTORY ARCHIVER]: usage is save iar []"); + return; + } + + string firstName = cmdparams[2]; + string lastName = cmdparams[3]; + string invPath = cmdparams[4]; + string savePath = (cmdparams.Length > 5 ? cmdparams[5] : DEFAULT_INV_BACKUP_FILENAME); + + ArchiveInventory(firstName, lastName, invPath, savePath); + } + } +} diff --git a/OpenSim/Region/Framework/Interfaces/IInventoryArchiverModule.cs b/OpenSim/Region/Framework/Interfaces/IInventoryArchiverModule.cs new file mode 100644 index 0000000..0e1e851 --- /dev/null +++ b/OpenSim/Region/Framework/Interfaces/IInventoryArchiverModule.cs @@ -0,0 +1,52 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * 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 OpenSim Project 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 DEVELOPERS ``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 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.IO; + +namespace OpenSim.Region.Framework.Interfaces +{ + public interface IInventoryArchiverModule + { + /// + /// Dearchive a user's inventory folder from the given stream + /// + /// + /// + /// The inventory path in which to place the loaded folders and items + /// The stream from which the inventory archive will be loaded + void DearchiveInventory(string firstName, string lastName, string invPath, Stream loadStream); + + /// + /// Archive a user's inventory folder to the given stream + /// + /// + /// + /// The inventory path from which the inventory should be saved. + /// The stream to which the inventory archive will be saved + void ArchiveInventory(string firstName, string lastName, string invPath, Stream saveStream); + } +} -- cgit v1.1