diff --git a/src/Orchard.Web/Modules/Orchard.DevTools/Controllers/CommandsController.cs b/src/Orchard.Web/Modules/Orchard.DevTools/Controllers/CommandsController.cs index 10fa6a5f7..e84d75225 100644 --- a/src/Orchard.Web/Modules/Orchard.DevTools/Controllers/CommandsController.cs +++ b/src/Orchard.Web/Modules/Orchard.DevTools/Controllers/CommandsController.cs @@ -42,5 +42,7 @@ namespace Orchard.DevTools.Controllers { model.Results = writer.ToString(); return View("Execute", model); } + + } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Orchard.Modules.csproj b/src/Orchard.Web/Modules/Orchard.Modules/Orchard.Modules.csproj index 0eb853f18..234c409b8 100644 --- a/src/Orchard.Web/Modules/Orchard.Modules/Orchard.Modules.csproj +++ b/src/Orchard.Web/Modules/Orchard.Modules/Orchard.Modules.csproj @@ -45,6 +45,7 @@ 3.5 + @@ -63,7 +64,9 @@ - + + True + @@ -73,9 +76,11 @@ + - - + + + @@ -94,6 +99,8 @@ + + diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Commands/PackagingCommands.cs b/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Commands/PackagingCommands.cs index e290cbd7c..d318ba9e1 100644 --- a/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Commands/PackagingCommands.cs +++ b/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Commands/PackagingCommands.cs @@ -1,10 +1,6 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net; -using System.Web; -using System.Web.Hosting; using Orchard.Commands; using Orchard.Environment.Extensions; using Orchard.Modules.Packaging.Services; @@ -12,55 +8,45 @@ using Orchard.Modules.Packaging.Services; namespace Orchard.Modules.Packaging.Commands { [OrchardFeature("Orchard.Modules.Packaging")] public class PackagingCommands : DefaultOrchardCommandHandler { + private readonly IExtensionManager _extensionManager; private readonly IPackageBuilder _packageBuilder; + private readonly IPackageManager _packageManager; - public PackagingCommands(IPackageBuilder packageBuilder) { + public PackagingCommands(IExtensionManager extensionManager, IPackageBuilder packageBuilder, IPackageManager packageManager) { + _extensionManager = extensionManager; _packageBuilder = packageBuilder; + _packageManager = packageManager; } [CommandHelp("harvest \r\n\t" + "Package a module into a distributable")] [CommandName("harvest")] public void PackageCreate(string moduleName) { - var stream = _packageBuilder.Create(moduleName); - if (stream.CanSeek) - stream.Seek(0, SeekOrigin.Begin); + var packageData = _packageManager.Harvest(moduleName); + if (packageData.PackageStream.CanSeek) + packageData.PackageStream.Seek(0, SeekOrigin.Begin); - using(var fileStream = new FileStream(HostingEnvironment.MapPath("~/Modules/" + moduleName + ".zip"), FileMode.Create, FileAccess.Write)) { - - const int chunk = 512; - var dataBuffer = new byte[3*chunk]; - var charBuffer = new char[4*chunk + 2]; - for (;;) { - var dataCount = stream.Read(dataBuffer, 0, dataBuffer.Length); - if (dataCount <= 0) - return; - - fileStream.Write(dataBuffer, 0, dataCount); - - var charCount = Convert.ToBase64CharArray(dataBuffer, 0, dataCount, charBuffer, 0); - Context.Output.Write(charBuffer, 0, charCount); - } + const int chunk = 512; + var dataBuffer = new byte[3 * chunk]; + var charBuffer = new char[4 * chunk + 2]; + for (; ; ) { + var dataCount = packageData.PackageStream.Read(dataBuffer, 0, dataBuffer.Length); + if (dataCount <= 0) + return; + var charCount = Convert.ToBase64CharArray(dataBuffer, 0, dataCount, charBuffer, 0); + Context.Output.Write(charBuffer, 0, charCount); } } [CommandHelp("harvest post \r\n\t" + "Package a module into a distributable and push it to a feed server.")] [CommandName("harvest post")] - public void PackageCreate(string moduleName, string feed) { - var stream = _packageBuilder.Create(moduleName); - if (stream.CanSeek) - stream.Seek(0, SeekOrigin.Begin); + public void PackageCreate(string moduleName, string feedUrl) { + var packageData = _packageManager.Harvest(moduleName); + _packageManager.Push(packageData, feedUrl); - var request = WebRequest.Create(feed); - request.Method = "POST"; - request.ContentType = "application/x-package"; - using (var requestStream = request.GetRequestStream()) { - stream.CopyTo(requestStream); - } try { - using (var response = request.GetResponse()) { - Context.Output.Write("Success: {0}", response.ResponseUri); - } + _packageManager.Push(packageData, feedUrl); + Context.Output.WriteLine("Success"); } catch (WebException webException) { var text = new StreamReader(webException.Response.GetResponseStream()).ReadToEnd(); diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Controllers/DownloadStreamResult.cs b/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Controllers/DownloadStreamResult.cs new file mode 100644 index 000000000..5f282702b --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Controllers/DownloadStreamResult.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Controllers/PackagingController.cs b/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Controllers/PackagingController.cs index 634e6297e..fdba015c4 100644 --- a/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Controllers/PackagingController.cs +++ b/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Controllers/PackagingController.cs @@ -1,46 +1,126 @@ using System; +using System.IO; +using System.Linq; using System.Web.Mvc; using Orchard.Environment.Extensions; +using Orchard.Localization; using Orchard.Modules.Packaging.Services; using Orchard.Modules.Packaging.ViewModels; using Orchard.Themes; using Orchard.UI.Admin; +using Orchard.UI.Notify; namespace Orchard.Modules.Packaging.Controllers { [Admin, Themed, OrchardFeature("Orchard.Modules.Packaging")] public class PackagingController : Controller { - private readonly IPackageRepository _packageRepository; + private readonly IPackageManager _packageManager; + private readonly IPackageSourceManager _packageSourceManager; + private readonly IExtensionManager _extensionManager; + private readonly INotifier _notifier; - public PackagingController(IPackageRepository packageRepository) { - _packageRepository = packageRepository; + public PackagingController( + IPackageManager packageManager, + IPackageSourceManager packageSourceManager, + IExtensionManager extensionManager, + INotifier notifier) { + _packageManager = packageManager; + _packageSourceManager = packageSourceManager; + _extensionManager = extensionManager; + _notifier = notifier; + T = NullLocalizer.Instance; } + Localizer T { get; set; } + public ActionResult Index() { return Modules(); } - public ActionResult Modules() { - return View("Modules", new PackagingIndexViewModel { - Modules = _packageRepository.GetModuleList() - }); - } - public ActionResult Sources() { - return View("Sources", new PackagingIndexViewModel { - Sources = _packageRepository.GetSources(), + return View("Sources", new PackagingSourcesViewModel { + Sources = _packageSourceManager.GetSources(), }); } public ActionResult AddSource(string url) { - _packageRepository.AddSource(new PackageSource { Id = Guid.NewGuid(), FeedUrl = url }); - return RedirectToAction("Index"); + _packageSourceManager.AddSource(new PackageSource { Id = Guid.NewGuid(), FeedUrl = url }); + Update(); + return RedirectToAction("Sources"); + } + + + public ActionResult Modules() { + return View("Modules", new PackagingModulesViewModel { + Modules = _packageSourceManager.GetModuleList() + }); } public ActionResult Update() { - _packageRepository.UpdateLists(); - //notifier + _packageSourceManager.UpdateLists(); + _notifier.Information(T("List of available modules and themes is updated.")); return RedirectToAction("Index"); } + public ActionResult Harvest(string extensionName, string feedUrl) { + return View("Harvest", new PackagingHarvestViewModel { + ExtensionName = extensionName, + FeedUrl = feedUrl, + Sources = _packageSourceManager.GetSources(), + Extensions = _extensionManager.AvailableExtensions() + }); + } + + [HttpPost] + public ActionResult Harvest(PackagingHarvestViewModel model) { + model.Sources = _packageSourceManager.GetSources(); + model.Extensions = _extensionManager.AvailableExtensions(); + + var packageData = _packageManager.Harvest(model.ExtensionName); + + if (string.IsNullOrEmpty(model.FeedUrl)) { + return new DownloadStreamResult( + packageData.ExtensionName + "-" + packageData.ExtensionVersion + ".zip", + "application/x-package", + packageData.PackageStream); + } + + if (!model.Sources.Any(src => src.FeedUrl == model.FeedUrl)) { + ModelState.AddModelError("FeedUrl", T("May only push directly to one of the configured sources.").ToString()); + return View("Harvest", model); + } + + _packageManager.Push(packageData, model.FeedUrl); + _notifier.Information(T("Harvested {0} and published onto {1}", model.ExtensionName, model.FeedUrl)); + + Update(); + + return RedirectToAction("Harvest", new { model.ExtensionName, model.FeedUrl }); + } + + public ActionResult Install(string syndicationId) { + var packageData = _packageManager.Download(syndicationId); + _packageManager.Install(packageData); + _notifier.Information(T("Installed module")); + return RedirectToAction("Modules"); + } + } + + public class DownloadStreamResult : ActionResult { + public string FileName { get; set; } + public string ContentType { get; set; } + public Stream Stream { get; set; } + + public DownloadStreamResult(string fileName, string contentType, Stream stream) { + FileName = fileName; + ContentType = contentType; + Stream = stream; + } + + public override void ExecuteResult(ControllerContext context) { + context.HttpContext.Response.ContentType = ContentType; + context.HttpContext.Response.AddHeader("content-disposition", "attachment; filename=\"" + FileName + "\""); + Stream.Seek(0, SeekOrigin.Begin); + Stream.CopyTo(context.HttpContext.Response.OutputStream); + } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Services/PackageBuilder.cs b/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Services/PackageBuilder.cs index 9579581b8..421ac015b 100644 --- a/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Services/PackageBuilder.cs +++ b/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Services/PackageBuilder.cs @@ -11,7 +11,7 @@ using Orchard.FileSystems.WebSite; namespace Orchard.Modules.Packaging.Services { public interface IPackageBuilder : IDependency { - Stream Create(string moduleName); + Stream BuildPackage(ExtensionDescriptor extensionDescriptor); } [OrchardFeature("Orchard.Modules.Packaging")] @@ -35,28 +35,26 @@ namespace Orchard.Modules.Packaging.Services { public XDocument Project { get; set; } } - public Stream Create(string moduleName) { - var extensionDescriptor = _extensionManager - .AvailableExtensions() - .FirstOrDefault(x => x.Name == moduleName); - return Create(extensionDescriptor); - } - - public Stream Create(ExtensionDescriptor extensionDescriptor) { - var projectFile = extensionDescriptor.Name + ".csproj"; + public Stream BuildPackage(ExtensionDescriptor extensionDescriptor) { + var context = new CreateContext(); BeginPackage(context); - EstablishPaths(context, _webSiteFolder, extensionDescriptor.Location, extensionDescriptor.Name); - SetCoreProperties(context, extensionDescriptor); + try { + EstablishPaths(context, _webSiteFolder, extensionDescriptor.Location, extensionDescriptor.Name); + SetCoreProperties(context, extensionDescriptor); - if (LoadProject(context, projectFile)) { - EmbedVirtualFile(context, projectFile, System.Net.Mime.MediaTypeNames.Text.Xml); - EmbedProjectFiles(context, "Compile", "Content", "None", "EmbeddedResource"); - EmbedReferenceFiles(context); + var projectFile = extensionDescriptor.Name + ".csproj"; + if (LoadProject(context, projectFile)) { + EmbedVirtualFile(context, projectFile, System.Net.Mime.MediaTypeNames.Text.Xml); + EmbedProjectFiles(context, "Compile", "Content", "None", "EmbeddedResource"); + EmbedReferenceFiles(context); + } + } + finally { + EndPackage(context); } - EndPackage(context); return context.Stream; } diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Services/PackageExpander.cs b/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Services/PackageExpander.cs new file mode 100644 index 000000000..36836cbe7 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Services/PackageExpander.cs @@ -0,0 +1,179 @@ +using System; +using System.IO; +using System.IO.Packaging; +using System.Linq; +using System.Reflection; +using System.Xml; +using System.Xml.Linq; +using Orchard.Environment.Extensions; +using Orchard.Environment.Extensions.Models; +using Orchard.FileSystems.VirtualPath; +using Orchard.FileSystems.WebSite; + +namespace Orchard.Modules.Packaging.Services { + public interface IPackageExpander : IDependency { + void ExpandPackage(Stream packageStream); + } + + [OrchardFeature("Orchard.Modules.Packaging")] + public class PackageExpander : IPackageExpander { + private const string ContentTypePrefix = "Orchard "; + private readonly IExtensionManager _extensionManager; + private readonly IWebSiteFolder _webSiteFolder; + private readonly IVirtualPathProvider _virtualPathProvider; + + public PackageExpander( + IExtensionManager extensionManager, + IWebSiteFolder webSiteFolder, + IVirtualPathProvider virtualPathProvider) { + _extensionManager = extensionManager; + _webSiteFolder = webSiteFolder; + _virtualPathProvider = virtualPathProvider; + } + + class ExpandContext { + public Stream Stream { get; set; } + public Package Package { get; set; } + + public string ExtensionName { get; set; } + public string ExtensionType { get; set; } + + public string TargetPath { get; set; } + + public string SourcePath { get; set; } + + public XDocument Project { get; set; } + } + + public void ExpandPackage(Stream packageStream) { + var context = new ExpandContext(); + BeginPackage(context, packageStream); + try { + GetCoreProperties(context); + EstablishPaths(context, _virtualPathProvider); + + var projectFile = context.ExtensionName + ".csproj"; + if (LoadProject(context, projectFile)) { + ExtractFile(context, projectFile); + ExtractProjectFiles(context, "Compile", "Content", "None", "EmbeddedResource"); + ExtractReferenceFiles(context); + } + } + finally { + EndPackage(context); + } + } + + private void ExtractFile(ExpandContext context, string relativePath) { + var partUri = PackUriHelper.CreatePartUri(new Uri(context.SourcePath + relativePath, UriKind.Relative)); + var packagePart = context.Package.GetPart(partUri); + using (var packageStream = packagePart.GetStream(FileMode.Open, FileAccess.Read)) { + var filePath = Path.Combine(context.TargetPath, relativePath); + var folderPath = Path.GetDirectoryName(filePath); + if (!Directory.Exists(folderPath)) + Directory.CreateDirectory(folderPath); + + using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read)) { + packageStream.CopyTo(fileStream); + } + } + } + + private void ExtractProjectFiles(ExpandContext context, params string[] itemGroupTypes) { + var itemGroups = context.Project + .Elements(Ns("Project")) + .Elements(Ns("ItemGroup")); + + foreach (var itemGroupType in itemGroupTypes) { + var includePaths = itemGroups + .Elements(Ns(itemGroupType)) + .Attributes("Include") + .Select(x => x.Value); + foreach (var includePath in includePaths) { + ExtractFile(context, includePath); + } + } + } + + private void ExtractReferenceFiles(ExpandContext context) { + var entries = context.Project + .Elements(Ns("Project")) + .Elements(Ns("ItemGroup")) + .Elements(Ns("Reference")) + .Select(reference => new { + Include = reference.Attribute("Include"), + HintPath = reference.Element(Ns("HintPath")) + }) + .Where(entry => entry.Include != null); + + foreach (var entry in entries) { + var assemblyName = new AssemblyName(entry.Include.Value); + var hintPath = entry.HintPath != null ? entry.HintPath.Value : null; + + var virtualPath = "bin/" + assemblyName.Name + ".dll"; + if (PartExists(context, virtualPath)) { + ExtractFile(context, virtualPath); + } + else if (hintPath != null) { + } + } + } + + private bool PartExists(ExpandContext context, string relativePath) { + var projectUri = PackUriHelper.CreatePartUri(new Uri(context.SourcePath + relativePath, UriKind.Relative)); + return context.Package.PartExists(projectUri); + } + + + private XName Ns(string localName) { + return XName.Get(localName, "http://schemas.microsoft.com/developer/msbuild/2003"); + } + private static bool LoadProject(ExpandContext context, string relativePath) { + var projectUri = PackUriHelper.CreatePartUri(new Uri(context.SourcePath + relativePath, UriKind.Relative)); + if (!context.Package.PartExists(projectUri)) + return false; + var part = context.Package.GetPart(projectUri); + using (var stream = part.GetStream(FileMode.Open, FileAccess.Read)) { + context.Project = XDocument.Load(stream); + } + return true; + } + + private void BeginPackage(ExpandContext context, Stream packageStream) { + if (packageStream.CanSeek) { + context.Stream = packageStream; + } + else { + context.Stream = new MemoryStream(); + packageStream.CopyTo(context.Stream); + } + context.Package = Package.Open(context.Stream, FileMode.Open, FileAccess.Read); + } + + private void EndPackage(ExpandContext context) { + context.Package.Close(); + } + + private void GetCoreProperties(ExpandContext context) { + context.ExtensionName = context.Package.PackageProperties.Identifier; + + var contentType = context.Package.PackageProperties.ContentType; + if (contentType.StartsWith(ContentTypePrefix)) + context.ExtensionType = contentType.Substring(ContentTypePrefix.Length); + } + + private void EstablishPaths(ExpandContext context, IVirtualPathProvider virtualPathProvider) { + context.SourcePath = "\\" + context.ExtensionName + "\\"; + if (context.ExtensionType == "Theme") { + context.TargetPath = virtualPathProvider.MapPath("~/Themes-temp/" + context.ExtensionName); + } + else if (context.ExtensionType == "Module") { + context.TargetPath = virtualPathProvider.MapPath("~/Modules-temp/" + context.ExtensionName); + } + else { + throw new ApplicationException("Unknown extension type"); + } + } + + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Services/PackageManager.cs b/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Services/PackageManager.cs new file mode 100644 index 000000000..bfd19a40a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Services/PackageManager.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Packaging; +using System.Linq; +using System.Net; +using System.Web; +using Orchard.Environment.Extensions; + +namespace Orchard.Modules.Packaging.Services { + + public class PackageData { + public string ExtensionName { get; set; } + public string ExtensionVersion { get; set; } + + public Stream PackageStream { get; set; } + } + + public interface IPackageManager : IDependency { + PackageData Harvest(string extensionName); + PackageData Download(string feedItemId); + + void Push(PackageData packageData, string feedUrl); + void Install(PackageData packageData); + } + + public class PackageManager : IPackageManager { + private readonly IExtensionManager _extensionManager; + private readonly IPackageSourceManager _packageSourceManager; + private readonly IPackageBuilder _packageBuilder; + private readonly IPackageExpander _packageExpander; + + public PackageManager( + IExtensionManager extensionManager, + IPackageSourceManager packageSourceManager, + IPackageBuilder packageBuilder, + IPackageExpander packageExpander) { + _extensionManager = extensionManager; + _packageSourceManager = packageSourceManager; + _packageBuilder = packageBuilder; + _packageExpander = packageExpander; + } + + public PackageData Harvest(string extensionName) { + var extensionDescriptor = _extensionManager.AvailableExtensions().FirstOrDefault(x => x.Name == extensionName); + if (extensionDescriptor == null) + return null; + return new PackageData { + ExtensionName = extensionDescriptor.Name, + ExtensionVersion = extensionDescriptor.Version, + PackageStream = _packageBuilder.BuildPackage(extensionDescriptor), + }; + } + + public void Push(PackageData packageData, string feedUrl) { + + var request = WebRequest.Create(feedUrl); + request.Method = "POST"; + request.ContentType = "application/x-package"; + using (var requestStream = request.GetRequestStream()) { + packageData.PackageStream.Seek(0, SeekOrigin.Begin); + packageData.PackageStream.CopyTo(requestStream); + } + + using (request.GetResponse()) { + // forces request and disposes results + } + } + + public PackageData Download(string feedItemId) { + var entry = _packageSourceManager.GetModuleList().Single(x => x.SyndicationItem.Id == feedItemId); + var request = WebRequest.Create(entry.PackageStreamUri); + using (var response = request.GetResponse()) { + using (var responseStream = response.GetResponseStream()) { + var stream = new MemoryStream(); + responseStream.CopyTo(stream); + var package = Package.Open(stream); + try { + return new PackageData { + ExtensionName = package.PackageProperties.Identifier, + ExtensionVersion = package.PackageProperties.Version, + PackageStream = stream + }; + } + finally { + package.Close(); + } + } + } + } + + public void Install(PackageData packageData) { + _packageExpander.ExpandPackage(packageData.PackageStream); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Services/PackageRepository.cs b/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Services/PackageSourceManager.cs similarity index 68% rename from src/Orchard.Web/Modules/Orchard.Modules/Packaging/Services/PackageRepository.cs rename to src/Orchard.Web/Modules/Orchard.Modules/Packaging/Services/PackageSourceManager.cs index d18f0213c..e9f0887cd 100644 --- a/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Services/PackageRepository.cs +++ b/src/Orchard.Web/Modules/Orchard.Modules/Packaging/Services/PackageSourceManager.cs @@ -2,20 +2,22 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.ServiceModel.Syndication; using System.Web; +using System.Xml; using System.Xml.Linq; using System.Xml.Serialization; using Orchard.Environment.Extensions; using Orchard.FileSystems.AppData; namespace Orchard.Modules.Packaging.Services { - public interface IPackageRepository : IDependency { + public interface IPackageSourceManager : IDependency { IEnumerable GetSources(); void AddSource(PackageSource source); void RemoveSource(Guid id); void UpdateLists(); - IEnumerable GetModuleList(); + IEnumerable GetModuleList(); } public class PackageSource { @@ -23,17 +25,13 @@ namespace Orchard.Modules.Packaging.Services { public string FeedUrl { get; set; } } - public class PackageInfo { + public class PackageEntry { public PackageSource Source { get; set; } - - public AtomEntry AtomEntry { get; set; } + public SyndicationFeed SyndicationFeed { get; set; } + public SyndicationItem SyndicationItem { get; set; } + public string PackageStreamUri { get; set; } } - public class AtomEntry { - public string Id { get; set; } - public string Title { get; set; } - public string Updated { get; set; } - } static class AtomExtensions { public static string Atom(this XElement entry, string localName) { @@ -47,11 +45,11 @@ namespace Orchard.Modules.Packaging.Services { } [OrchardFeature("Orchard.Modules.Packaging")] - public class PackageRepository : IPackageRepository { + public class PackageSourceManager : IPackageSourceManager { private readonly IAppDataFolder _appDataFolder; private static readonly XmlSerializer _sourceSerializer = new XmlSerializer(typeof(List), new XmlRootAttribute("Sources")); - public PackageRepository(IAppDataFolder appDataFolder) { + public PackageSourceManager(IAppDataFolder appDataFolder) { _appDataFolder = appDataFolder; } @@ -103,39 +101,39 @@ namespace Orchard.Modules.Packaging.Services { return AtomExtensions.AtomXName(localName); } - public IEnumerable GetModuleList() { - var packageInfos = GetSources() - .SelectMany( - source => - Bind(_appDataFolder.ReadFile(GetFeedCachePath(source)), - content => - XDocument.Parse(content) - .Elements(Atom("feed")) - .Elements(Atom("entry")) - .SelectMany( - element => - Bind(new AtomEntry { - Id = element.Atom("id"), - Title = element.Atom("title"), - Updated = element.Atom("updated"), - }, - atom => - Unit(new PackageInfo { - Source = source, - AtomEntry = atom, - }))))); - - return packageInfos.ToArray(); - } - - static IEnumerable Unit(T t) where T : class { return t != null ? new[] { t } : Enumerable.Empty(); } static IEnumerable Bind(T t, Func> f) where T : class { return Unit(t).SelectMany(f); } + + private SyndicationFeed ParseFeed(string content) { + var formatter = new Atom10FeedFormatter(); + formatter.ReadFrom(XmlReader.Create(new StringReader(content))); + return formatter.Feed; + } + + public IEnumerable GetModuleList() { + var packageInfos = GetSources() + .SelectMany( + source => + Bind(ParseFeed(_appDataFolder.ReadFile(GetFeedCachePath(source))), + feed => + feed.Items.SelectMany( + item => + Unit(new PackageEntry { + Source = source, + SyndicationFeed = feed, + SyndicationItem = item, + PackageStreamUri = item.Links.Single().GetAbsoluteUri().AbsoluteUri, + })))); + + + return packageInfos.ToArray(); + } + + } - } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Packaging/ViewModels/PackagingIndexViewModel.cs b/src/Orchard.Web/Modules/Orchard.Modules/Packaging/ViewModels/PackagingIndexViewModel.cs deleted file mode 100644 index 9d719fe9a..000000000 --- a/src/Orchard.Web/Modules/Orchard.Modules/Packaging/ViewModels/PackagingIndexViewModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using Orchard.Modules.Packaging.Services; - -namespace Orchard.Modules.Packaging.ViewModels { - public class PackagingIndexViewModel { - public IEnumerable Sources { get; set; } - public IEnumerable Modules { get; set; } - } -} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Packaging/ViewModels/PackagingModulesViewModel.cs b/src/Orchard.Web/Modules/Orchard.Modules/Packaging/ViewModels/PackagingModulesViewModel.cs new file mode 100644 index 000000000..a2f130643 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Modules/Packaging/ViewModels/PackagingModulesViewModel.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Web; +using Orchard.Environment.Extensions.Models; +using Orchard.Modules.Packaging.Services; + +namespace Orchard.Modules.Packaging.ViewModels { + public class PackagingModulesViewModel { + public IEnumerable Modules { get; set; } + } + public class PackagingSourcesViewModel { + public IEnumerable Sources { get; set; } + } + public class PackagingHarvestViewModel { + public IEnumerable Sources { get; set; } + public IEnumerable Extensions { get; set; } + + [Required] + public string ExtensionName { get; set; } + + public string FeedUrl { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Views/Packaging/Harvest.ascx b/src/Orchard.Web/Modules/Orchard.Modules/Views/Packaging/Harvest.ascx new file mode 100644 index 000000000..4b7dd74b6 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Modules/Views/Packaging/Harvest.ascx @@ -0,0 +1,29 @@ +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ Import Namespace="Orchard.Mvc.Html" %> +

+ <%: Html.TitleForPage(T("Packaging").ToString(), T("Harvest Packages").ToString())%>

+ <%: Html.Partial("_Subnav") %> + +<%using (Html.BeginFormAntiForgeryPost()) {%> +<%: Html.ValidationSummary(T("Package creation was unsuccessful. Please correct the errors and try again.").ToString()) %> +<%foreach (var group in Model.Extensions.Where(x => !x.Location.StartsWith("~/Core")).GroupBy(x => x.ExtensionType)) {%> +
+ Harvest + <%:group.Key %> +
    + <%foreach (var item in group) {%> +
  • +
  • <% + }%>
+ <%} %> + <%: Html.ValidationMessageFor(m => m.ExtensionName)%> +
+
+ <%: Html.LabelFor(m=>m.FeedUrl)%> + <%: Html.DropDownListFor(m => m.FeedUrl, new[]{new SelectListItem{Text="Download",Value=""}}.Concat( Model.Sources.Select(x => new SelectListItem { Text = "Push to " + x.FeedUrl, Value = x.FeedUrl })))%> + <%: Html.ValidationMessageFor(m=>m.FeedUrl) %> +
+ +<%} %> diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Views/Packaging/Modules.ascx b/src/Orchard.Web/Modules/Orchard.Modules/Views/Packaging/Modules.ascx index e52aaf4f8..08408aad0 100644 --- a/src/Orchard.Web/Modules/Orchard.Modules/Views/Packaging/Modules.ascx +++ b/src/Orchard.Web/Modules/Orchard.Modules/Views/Packaging/Modules.ascx @@ -1,7 +1,12 @@ -<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> -<%@ Import Namespace="Orchard.Mvc.Html"%> -

<%: Html.TitleForPage(T("Packages").ToString()) %>

-

<%:Html.ActionLink("Update List", "Update") %> • <%:Html.ActionLink("Edit Sources", "Sources") %>

-
    <%foreach (var item in Model.Modules) {%>
  • <%:item.AtomEntry.Title %>
  • <% - }%>
+<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ Import Namespace="Orchard.Mvc.Html" %> +

+ <%: Html.TitleForPage(T("Packaging").ToString(), T("Browse Packages").ToString())%>

+ <%: Html.Partial("_Subnav") %> +

<%:Html.ActionLink("Update List", "Update") %>

+
    + <%foreach (var item in Model.Modules) {%>
  • + <%:item.SyndicationItem.Title.Text%> [<%:Html.ActionLink("Install", "Install", new RouteValueDictionary {{"SyndicationId",item.SyndicationItem.Id}})%>] +
  • <% + }%>
diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Views/Packaging/Sources.ascx b/src/Orchard.Web/Modules/Orchard.Modules/Views/Packaging/Sources.ascx index f5cf543f9..3eaa9f6cb 100644 --- a/src/Orchard.Web/Modules/Orchard.Modules/Views/Packaging/Sources.ascx +++ b/src/Orchard.Web/Modules/Orchard.Modules/Views/Packaging/Sources.ascx @@ -1,7 +1,9 @@ -<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> <%@ Import Namespace="Orchard.Mvc.Html" %>

- <%: Html.TitleForPage(T("Packages").ToString()) %>

+ <%: Html.TitleForPage(T("Packaging").ToString(), T("Edit Sources").ToString())%> + <%: Html.Partial("_Subnav") %> +
    <%foreach (var item in Model.Sources) {%>
  • <%:Html.Link(item.FeedUrl, item.FeedUrl)%>
  • <% diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Views/Packaging/_Subnav.ascx b/src/Orchard.Web/Modules/Orchard.Modules/Views/Packaging/_Subnav.ascx new file mode 100644 index 000000000..143bf7e19 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Modules/Views/Packaging/_Subnav.ascx @@ -0,0 +1,8 @@ +<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> +

    + <%:Html.ActionLink("Browse Repository Packages", "Index") %> + • + <%:Html.ActionLink("Harvest Local Packages", "Harvest") %> + • + <%:Html.ActionLink("Edit Repository Sources", "Sources") %> +

    diff --git a/src/Orchard/Commands/CommandContext.cs b/src/Orchard/Commands/CommandContext.cs index 8f1ed039f..35ae441d3 100644 --- a/src/Orchard/Commands/CommandContext.cs +++ b/src/Orchard/Commands/CommandContext.cs @@ -7,9 +7,12 @@ namespace Orchard.Commands { public class CommandContext { public TextReader Input { get; set; } public TextWriter Output { get; set; } + + public string Command { get; set; } public IEnumerable Arguments { get; set; } public IDictionary Switches { get; set; } + public CommandDescriptor CommandDescriptor { get; set; } } } diff --git a/src/Tools/PackageIndexReferenceImplementation/Controllers/Artifacts/AtomFeedResult.cs b/src/Tools/PackageIndexReferenceImplementation/Controllers/Artifacts/AtomFeedResult.cs new file mode 100644 index 000000000..beac61c0f --- /dev/null +++ b/src/Tools/PackageIndexReferenceImplementation/Controllers/Artifacts/AtomFeedResult.cs @@ -0,0 +1,21 @@ +using System.ServiceModel.Syndication; +using System.Web.Mvc; +using System.Xml; + +namespace PackageIndexReferenceImplementation.Controllers.Artifacts { + public class AtomFeedResult : ActionResult { + public SyndicationFeed Feed { get; set; } + + public AtomFeedResult(SyndicationFeed feed) { + Feed = feed; + } + + public override void ExecuteResult(ControllerContext context) { + context.HttpContext.Response.ContentType = "application/atom+xml"; + using (var writer = XmlWriter.Create(context.HttpContext.Response.OutputStream)) { + var formatter = new Atom10FeedFormatter(Feed); + formatter.WriteTo(writer); + } + } + } +} \ No newline at end of file diff --git a/src/Tools/PackageIndexReferenceImplementation/Controllers/Artifacts/AtomItemResult.cs b/src/Tools/PackageIndexReferenceImplementation/Controllers/Artifacts/AtomItemResult.cs new file mode 100644 index 000000000..cfc02c5c5 --- /dev/null +++ b/src/Tools/PackageIndexReferenceImplementation/Controllers/Artifacts/AtomItemResult.cs @@ -0,0 +1,27 @@ +using System.ServiceModel.Syndication; +using System.Web.Mvc; +using System.Xml; + +namespace PackageIndexReferenceImplementation.Controllers.Artifacts { + public class AtomItemResult : ActionResult { + public string Status { get; set; } + public string Location { get; set; } + public SyndicationItem Item { get; set; } + + public AtomItemResult(string status, string location, SyndicationItem item) { + Status = status; + Location = location; + Item = item; + } + + public override void ExecuteResult(ControllerContext context) { + context.HttpContext.Response.Status = Status; + context.HttpContext.Response.RedirectLocation = Location; + context.HttpContext.Response.ContentType = "application/atom+xml;type=entry"; + using (var writer = XmlWriter.Create(context.HttpContext.Response.OutputStream)) { + var formatter = new Atom10ItemFormatter(Item); + formatter.WriteTo(writer); + } + } + } +} \ No newline at end of file diff --git a/src/Tools/PackageIndexReferenceImplementation/Controllers/Artifacts/ContentTypeAttribute.cs b/src/Tools/PackageIndexReferenceImplementation/Controllers/Artifacts/ContentTypeAttribute.cs new file mode 100644 index 000000000..fb29f2bca --- /dev/null +++ b/src/Tools/PackageIndexReferenceImplementation/Controllers/Artifacts/ContentTypeAttribute.cs @@ -0,0 +1,18 @@ +using System; +using System.Reflection; +using System.Web.Mvc; + +namespace PackageIndexReferenceImplementation.Controllers.Artifacts { + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class ContentTypeAttribute : ActionMethodSelectorAttribute { + public ContentTypeAttribute(string contentType) { + ContentType = contentType; + } + + public string ContentType { get; set; } + + public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) { + return controllerContext.HttpContext.Request.ContentType.StartsWith(ContentType); + } + } +} \ No newline at end of file diff --git a/src/Tools/PackageIndexReferenceImplementation/Controllers/Artifacts/XmlBodyAttribute.cs b/src/Tools/PackageIndexReferenceImplementation/Controllers/Artifacts/XmlBodyAttribute.cs new file mode 100644 index 000000000..aba4a0d81 --- /dev/null +++ b/src/Tools/PackageIndexReferenceImplementation/Controllers/Artifacts/XmlBodyAttribute.cs @@ -0,0 +1,13 @@ +using System; +using System.Web.Mvc; +using System.Xml.Linq; + +namespace PackageIndexReferenceImplementation.Controllers.Artifacts { + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class XmlBodyAttribute : ActionFilterAttribute { + public override void OnActionExecuting(ActionExecutingContext filterContext) { + var body = XElement.Load(filterContext.HttpContext.Request.InputStream); + filterContext.ActionParameters["body"] = body.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Tools/PackageIndexReferenceImplementation/Controllers/AtomController.cs b/src/Tools/PackageIndexReferenceImplementation/Controllers/AtomController.cs index b5760bc28..babf1211d 100644 --- a/src/Tools/PackageIndexReferenceImplementation/Controllers/AtomController.cs +++ b/src/Tools/PackageIndexReferenceImplementation/Controllers/AtomController.cs @@ -1,42 +1,27 @@ using System; using System.IO; using System.IO.Packaging; -using System.Reflection; +using System.Linq; +using System.Security.Policy; using System.Web.Mvc; -using System.Xml; -using System.Xml.Linq; +using System.Web.Routing; using System.ServiceModel.Syndication; +using PackageIndexReferenceImplementation.Controllers.Artifacts; +using PackageIndexReferenceImplementation.Services; namespace PackageIndexReferenceImplementation.Controllers { - public class SyndicationResult : ActionResult { - public SyndicationFeedFormatter Formatter { get; set; } - - public SyndicationResult(SyndicationFeedFormatter formatter) { - Formatter = formatter; - } - - public override void ExecuteResult(ControllerContext context) { - context.HttpContext.Response.ContentType = "application/atom+xml"; - using (var writer = XmlWriter.Create(context.HttpContext.Response.OutputStream)) { - Formatter.WriteTo(writer); - } - } - } - [HandleError] public class AtomController : Controller { - public ActionResult Index() { - var feed = new SyndicationFeed { - Items = new[] { - new SyndicationItem { - Id = "hello", - Title = new TextSyndicationContent("Orchard.Media", TextSyndicationContentKind.Plaintext), - LastUpdatedTime = DateTimeOffset.UtcNow - } - } - }; + private readonly FeedStorage _feedStorage; + private readonly MediaStorage _mediaStorage; - return new SyndicationResult(new Atom10FeedFormatter(feed)); + public AtomController() { + _feedStorage = new FeedStorage(); + _mediaStorage = new MediaStorage(); + } + + public ActionResult Index() { + return new AtomFeedResult(_feedStorage.GetFeed()); } [ActionName("Index"), HttpPost, ContentType("application/atom+xml"), XmlBody] @@ -46,37 +31,68 @@ namespace PackageIndexReferenceImplementation.Controllers { [ActionName("Index"), HttpPost, ContentType("application/x-package")] public ActionResult PostPackage() { + + var hostHeader = HttpContext.Request.Headers["Host"]; + var slugHeader = HttpContext.Request.Headers["Slug"]; + var utcNowDateString = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd"); + var package = Package.Open(Request.InputStream, FileMode.Open, FileAccess.Read); + var packageProperties = package.PackageProperties; - return RedirectToAction("Index"); + var feed = _feedStorage.GetFeed(); + + var item = feed.Items.FirstOrDefault(i => i.Id.StartsWith("tag:") && i.Id.EndsWith(":" + packageProperties.Identifier)); + if (item == null) { + item = new SyndicationItem { + Id = "tag:" + hostHeader + "," + utcNowDateString + ":" + packageProperties.Identifier + }; + feed.Items = feed.Items.Concat(new[] { item }); + } + + + if (!string.IsNullOrEmpty(packageProperties.Category)) { + item.Authors.Clear(); + //parse package.PackageProperties.Creator into email-style authors + item.Authors.Add(new SyndicationPerson { Name = packageProperties.Creator }); + } + + if (!string.IsNullOrEmpty(packageProperties.Category)) { + item.Categories.Clear(); + item.Categories.Add(new SyndicationCategory(packageProperties.Category)); + } + + if (packageProperties.Modified.HasValue) { + item.LastUpdatedTime = new DateTimeOffset(packageProperties.Modified.Value); + } + + if (!string.IsNullOrEmpty(packageProperties.Title)) { + item.Title = new TextSyndicationContent(packageProperties.Title); + } + + if (!string.IsNullOrEmpty(packageProperties.Description)) { + item.Summary = new TextSyndicationContent(packageProperties.Description); + } + + if (!string.IsNullOrEmpty(packageProperties.Title)) { + item.Title = new TextSyndicationContent(packageProperties.Title); + } + + var mediaIdentifier = packageProperties.Identifier + "-" + packageProperties.Version + ".zip"; + + var mediaUrl = Url.Action("Resource", "Media", new RouteValueDictionary { { "Id", mediaIdentifier }, { "ContentType", "application/x-package" } }); + item.Links.Clear(); + item.Links.Add(new SyndicationLink(new Uri(HostBaseUri(), new Uri(mediaUrl, UriKind.Relative)))); + + Request.InputStream.Seek(0, SeekOrigin.Begin); + _mediaStorage.StoreMedia(mediaIdentifier+":application/x-package", Request.InputStream); + _feedStorage.StoreFeed(feed); + + return new AtomItemResult("201 Created", null, item); } - static XElement Atom(string localName, params XNode[] content) { - return new XElement(XName.Get(localName, "http://www.w3.org/2005/Atom"), content); - } - static XElement Atom(string localName, string value) { - return new XElement(XName.Get(localName, "http://www.w3.org/2005/Atom"), new XText(value)); - } - } - - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] - public class ContentTypeAttribute : ActionMethodSelectorAttribute { - public ContentTypeAttribute(string contentType) { - ContentType = contentType; + private Uri HostBaseUri() { + return new Uri("http://" + HttpContext.Request.Headers["Host"]); } - public string ContentType { get; set; } - - public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) { - return controllerContext.HttpContext.Request.ContentType.StartsWith(ContentType); - } - } - - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] - public class XmlBodyAttribute : ActionFilterAttribute { - public override void OnActionExecuting(ActionExecutingContext filterContext) { - var body = XElement.Load(filterContext.HttpContext.Request.InputStream); - filterContext.ActionParameters["body"] = body.ToString(); - } } } diff --git a/src/Tools/PackageIndexReferenceImplementation/Controllers/MediaController.cs b/src/Tools/PackageIndexReferenceImplementation/Controllers/MediaController.cs new file mode 100644 index 000000000..612d639fe --- /dev/null +++ b/src/Tools/PackageIndexReferenceImplementation/Controllers/MediaController.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Web; +using System.Web.Mvc; +using PackageIndexReferenceImplementation.Services; + +namespace PackageIndexReferenceImplementation.Controllers +{ + public class MediaController : Controller + { + private readonly MediaStorage _mediaStorage; + + public MediaController() { + _mediaStorage = new MediaStorage(); + } + + public ActionResult Resource(string id, string contentType) + { + return new StreamResult(contentType, _mediaStorage.GetMedia(id + ":" + contentType)); + } + } + + public class StreamResult : ActionResult { + public string ContentType { get; set; } + public Stream Stream { get; set; } + + public StreamResult(string contentType, Stream stream) { + ContentType = contentType; + Stream = stream; + } + + public override void ExecuteResult(ControllerContext context) { + context.HttpContext.Response.ContentType = ContentType; + Stream.CopyTo(context.HttpContext.Response.OutputStream); + } + } +} diff --git a/src/Tools/PackageIndexReferenceImplementation/PackageIndexReferenceImplementation.csproj b/src/Tools/PackageIndexReferenceImplementation/PackageIndexReferenceImplementation.csproj index 0de509bdd..9b877034e 100644 --- a/src/Tools/PackageIndexReferenceImplementation/PackageIndexReferenceImplementation.csproj +++ b/src/Tools/PackageIndexReferenceImplementation/PackageIndexReferenceImplementation.csproj @@ -69,12 +69,19 @@ + + + + + Global.asax + + diff --git a/src/Tools/PackageIndexReferenceImplementation/Services/FeedStorage.cs b/src/Tools/PackageIndexReferenceImplementation/Services/FeedStorage.cs new file mode 100644 index 000000000..e226be531 --- /dev/null +++ b/src/Tools/PackageIndexReferenceImplementation/Services/FeedStorage.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.ServiceModel.Syndication; +using System.Web; +using System.Web.Hosting; +using System.Xml; + +namespace PackageIndexReferenceImplementation.Services { + public class FeedStorage { + + public SyndicationFeed GetFeed() { + var formatter = new Atom10FeedFormatter(); + var feedPath = HostingEnvironment.MapPath("~/App_Data/Feed.xml"); + if (!File.Exists(feedPath)) { + return new SyndicationFeed(); + } + using (var reader = XmlReader.Create(feedPath)) { + formatter.ReadFrom(reader); + return formatter.Feed; + } + } + + public void StoreFeed(SyndicationFeed feed) { + var formatter = new Atom10FeedFormatter(feed); + var feedPath = HostingEnvironment.MapPath("~/App_Data/Feed.xml"); + using (var writer = XmlWriter.Create(feedPath)) { + formatter.WriteTo(writer); + } + } + } +} \ No newline at end of file diff --git a/src/Tools/PackageIndexReferenceImplementation/Services/MediaStorage.cs b/src/Tools/PackageIndexReferenceImplementation/Services/MediaStorage.cs new file mode 100644 index 000000000..1728c3db4 --- /dev/null +++ b/src/Tools/PackageIndexReferenceImplementation/Services/MediaStorage.cs @@ -0,0 +1,36 @@ +using System.IO; +using System.Linq; +using System.Web.Hosting; + +namespace PackageIndexReferenceImplementation.Services { + public class MediaStorage { + public void StoreMedia(string identifier, Stream data) { + if (!Directory.Exists(HostingEnvironment.MapPath("~/App_Data/Media"))) + Directory.CreateDirectory(HostingEnvironment.MapPath("~/App_Data/Media")); + + var safeIdentifier = GetSafeIdentifier(identifier); + var filePath = HostingEnvironment.MapPath("~/App_Data/Media/" + safeIdentifier); + using (var destination = new FileStream(filePath, FileMode.Create, FileAccess.Write)) { + data.CopyTo(destination); + } + } + + public Stream GetMedia(string identifier) { + if (!Directory.Exists(HostingEnvironment.MapPath("~/App_Data/Media"))) + Directory.CreateDirectory(HostingEnvironment.MapPath("~/App_Data/Media")); + + var safeIdentifier = GetSafeIdentifier(identifier); + var filePath = HostingEnvironment.MapPath("~/App_Data/Media/" + safeIdentifier); + return new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + } + + static string GetSafeIdentifier(string identifier) { + var invalidFileNameChars = Path.GetInvalidFileNameChars().Concat(Path.GetInvalidPathChars()).Distinct(); + var safeIdentifier = identifier.Replace("^", string.Format("^{0:X2}", (int)'^')); + foreach (var ch in invalidFileNameChars) { + safeIdentifier = safeIdentifier.Replace(new string(ch, 1), string.Format("^{0:X2}", (int)ch)); + } + return safeIdentifier; + } + } +} \ No newline at end of file