├── .gitignore ├── Build ├── AcquireLock.cs ├── Build.csproj ├── ColorExec.cs ├── Command.cs ├── Common.targets ├── Program.cs └── VerifyCoverage.cs ├── CommandPrompt.bat ├── LICENSE.txt ├── ZipDeploy.TestApp2_1 ├── HomeController.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── ZipDeploy.TestApp2_1.csproj ├── appsettings.Development.json ├── appsettings.json ├── bundleconfig.json └── wwwroot │ └── test.js ├── ZipDeploy.TestApp2_1Exe ├── HomeController.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── ZipDeploy.TestApp2_1Exe.csproj ├── appsettings.Development.json ├── appsettings.json ├── bundleconfig.json └── wwwroot │ └── test.js ├── ZipDeploy.TestApp3_1 ├── HomeController.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── ZipDeploy.TestApp3_1.csproj ├── appsettings.Development.json ├── appsettings.json └── wwwroot │ └── test.js ├── ZipDeploy.Tests ├── CoverageFilter.txt ├── DetectionTests.cs ├── FileSystem.cs ├── Test.cs ├── TestApp │ ├── Exec.cs │ ├── IisAdmin.cs │ ├── IisTests.cs │ └── Wait.cs ├── UnzipperTests.cs └── ZipDeploy.Tests.csproj ├── ZipDeploy.sln ├── ZipDeploy ├── Application.cs ├── CanPauseTrigger.cs ├── Cleaner.cs ├── DetectPackage.cs ├── FsUtil.cs ├── LockProcess.cs ├── LoggerExtensions.cs ├── ProcessWebConfig.cs ├── Registration.cs ├── TriggerRestart.cs ├── Unzipper.cs ├── ZipDeploy.cs ├── ZipDeploy.csproj └── ZipDeployOptions.cs ├── appveyor.yml ├── docs ├── _config.yml ├── googlebf97859327ca037d.html ├── index.md ├── quickstart.md └── walkthrough.md ├── global.json ├── icon.png ├── lib └── NuGet │ └── NuGet.exe └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | */bin 3 | */obj 4 | */*.csproj.user 5 | *.suo 6 | *.sln.cache 7 | /.vs 8 | /.vscode 9 | 10 | /_output 11 | /ZipDeploy.TestApp2_1Exe/logs/ 12 | /ZipDeploy.TestApp2_1/logs/ 13 | /ZipDeploy.TestApp3_1/logs/ 14 | -------------------------------------------------------------------------------- /Build/AcquireLock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | 5 | namespace Build 6 | { 7 | public class AcquireLock : Command 8 | { 9 | public override void Execute(Stack args) 10 | { 11 | if (args.Count != 1) 12 | throw new Exception($"usage: bin\\Build.exe AcquireLock AcquireLock "); 13 | 14 | var lockName = args.Pop(); 15 | 16 | UsingConsoleColor(ConsoleColor.Gray, () => Console.WriteLine($"acquiring on lock = '{lockName}'")); 17 | 18 | bool createdNew = false; 19 | Semaphore semaphore = null; 20 | 21 | while (!createdNew) 22 | { 23 | semaphore = new Semaphore(1, 1, lockName, out createdNew); 24 | 25 | if (!createdNew) 26 | using (semaphore) 27 | Thread.Sleep(200); 28 | } 29 | 30 | UsingConsoleColor(ConsoleColor.Gray, () => Console.WriteLine($"semaphore created createdNew={createdNew}")); 31 | UsingConsoleColor(ConsoleColor.Green, () => Console.WriteLine($"acquired lock = '{lockName}' - press enter to release")); 32 | Console.ReadLine(); 33 | 34 | UsingConsoleColor(ConsoleColor.Gray, () => Console.WriteLine($"process exiting")); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Build/Build.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Exe 7 | net461 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | true 35 | Debug 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | $(MSBuildThisFileDirectory)..\lib\NuGet\nuget.exe 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /Build/ColorExec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using Microsoft.Build.Framework; 4 | using Microsoft.Build.Utilities; 5 | 6 | namespace Build 7 | { 8 | public class ColorExec : Task 9 | { 10 | [Required] 11 | public string FileName { get; set; } 12 | 13 | public string Arguments { get; set; } 14 | public string WorkingDirectory { get; set; } 15 | 16 | public override bool Execute() 17 | { 18 | try 19 | { 20 | Log.LogMessage(MessageImportance.Normal, $"ColorExec WorkingDirectory='{WorkingDirectory}' FileName='{FileName}' Arguments='{Arguments}'"); 21 | 22 | using (var process = new Process()) 23 | { 24 | process.StartInfo.FileName = FileName; 25 | process.StartInfo.Arguments = Arguments; 26 | process.StartInfo.WorkingDirectory = WorkingDirectory; 27 | process.StartInfo.UseShellExecute = false; 28 | process.Start(); 29 | process.WaitForExit(); 30 | 31 | return process.ExitCode == 0; 32 | } 33 | } 34 | catch (Exception e) 35 | { 36 | Log.LogError($"Error: {e}"); 37 | return false; 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Build/Command.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Build 6 | { 7 | public abstract class Command 8 | { 9 | public static Action TryConsole = (Action action) => 10 | { 11 | try 12 | { 13 | action(); 14 | } 15 | catch (Exception e) 16 | { 17 | UsingConsoleColor(ConsoleColor.Red, () => Console.Error.WriteLine(e.Message)); 18 | Console.Error.WriteLine(e); 19 | Environment.Exit(1); 20 | } 21 | }; 22 | 23 | public static void UsingConsoleColor(ConsoleColor color, Action action) 24 | { 25 | var previousColor = Console.ForegroundColor; 26 | 27 | try 28 | { 29 | Console.ForegroundColor = color; 30 | action(); 31 | } 32 | finally 33 | { 34 | Console.ForegroundColor = previousColor; 35 | } 36 | } 37 | 38 | public static void Execute(string[] args) 39 | { 40 | var argStack = new Stack(new Stack(args)); 41 | 42 | if (argStack.Count == 0) 43 | throw new Exception("please supply the command as the first argument"); 44 | 45 | var commandName = argStack.Pop(); 46 | 47 | var commandType = typeof(Command).Assembly.GetTypes() 48 | .Where(t => t.Name == commandName) 49 | .SingleOrDefault(); 50 | 51 | if (commandType == null) 52 | throw new Exception("Could not load type: " + commandName); 53 | 54 | var command = (Command)Activator.CreateInstance(commandType); 55 | command.Execute(argStack); 56 | } 57 | 58 | public abstract void Execute(Stack args); 59 | 60 | protected void Retry(int numberOfTimes, Action method) 61 | { 62 | int timesLeft = numberOfTimes; 63 | while (timesLeft > 0) 64 | { 65 | try 66 | { 67 | method(); 68 | timesLeft = 0; 69 | } 70 | catch (Exception e) 71 | { 72 | timesLeft--; 73 | Console.WriteLine(string.Format("Failed ({0}) - {1} times left to retry", e.Message, timesLeft)); 74 | 75 | if (timesLeft <= 0) 76 | throw; 77 | } 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Build/Common.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 1.1.5 9 | Deploy updates to a running Asp.Net Core IIS application by uploading a zip file. 10 | Asp.Net Core Mvc Zip Deploy IIS 11 | Richard Brown 12 | MIT 13 | icon.png 14 | https://github.com/FlukeFan/ZipDeploy 15 | 16 | 80 17 | 18 | $(NuGetPackageRoot)\opencover\4.7.922\tools\OpenCover.Console.exe 19 | $(MSBuildExtensionsPath)vstest.console.dll 20 | $(NuGetPackageRoot)\nunit.consolerunner\3.11.1\tools\nunit3-console.exe 21 | $(NuGetPackageRoot)\reportgenerator\4.6.2\tools\netcoreapp3.0\ReportGenerator.exe 22 | 23 | %2A 24 | $(AssemblyName).dll 25 | $(AssemblyName).success.flg 26 | $(AssemblyName).coverage.xml 27 | 28 | -filterfile:"$(MSBuildProjectDirectory)\CoverageFilter.txt" 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | --where "$(FilterTest)" 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | $([System.DateTime]::Now.AddSeconds(1).ToString(yyyy-MM-dd HH:mm:ss)) 58 | --where ""$(FilterTest)"" 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /Build/Program.cs: -------------------------------------------------------------------------------- 1 | namespace Build 2 | { 3 | class Program 4 | { 5 | static void Main(string[] args) 6 | { 7 | Command.TryConsole(() => 8 | { 9 | Command.Execute(args); 10 | }); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Build/VerifyCoverage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Xml; 4 | 5 | namespace Build 6 | { 7 | public class VerifyCoverage : Command 8 | { 9 | public override void Execute(Stack args) 10 | { 11 | if (args.Count != 2) 12 | throw new Exception($"usage: dotnet Build.dll VerifyCoverage "); 13 | 14 | var targetPercentage = double.Parse(args.Pop()); 15 | var coverageFile = args.Pop(); 16 | 17 | var doc = new XmlDocument(); 18 | doc.Load(coverageFile); 19 | 20 | var lineCoverageNode = doc.SelectSingleNode("/CoverageReport/Summary/Linecoverage"); 21 | 22 | if (lineCoverageNode == null || string.IsNullOrWhiteSpace(lineCoverageNode.InnerText)) 23 | throw new Exception($"Could not find line coverage in {coverageFile}. Are you missing Full for a .Net Core test suite?"); 24 | 25 | var actualLineCoverage = double.Parse(lineCoverageNode.InnerText.Replace("%", "")); 26 | 27 | if (actualLineCoverage < targetPercentage) 28 | throw new Exception($"Expected at least {targetPercentage}% coverage, only got {actualLineCoverage}% coverage"); 29 | 30 | UsingConsoleColor(ConsoleColor.Green, () => Console.WriteLine($"Coverage of {actualLineCoverage}% is greater than target of {targetPercentage}% ")); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /CommandPrompt.bat: -------------------------------------------------------------------------------- 1 | @CD /D "%~dp0" 2 | @title ZipDeploy Command Prompt 3 | @SET PATH=C:\Program Files\dotnet\;%PATH% 4 | type readme.md 5 | @doskey bc=dotnet clean 6 | @doskey btw=dotnet watch msbuild /p:FilterTest="test =~ $1" /p:NoCoverage=true $2 $3 $4 $5 $6 $7 $8 $9 7 | @doskey bt=dotnet msbuild /p:FilterTest="test =~ $1" /p:NoCoverage=true $2 $3 $4 $5 $6 $7 $8 $9 8 | @doskey bw=dotnet watch msbuild /p:FilterTest="cat != Slow" $* 9 | @doskey ba=dotnet msbuild $* 10 | @doskey b=dotnet msbuild /p:FilterTest="cat != Slow" $* 11 | @doskey br=dotnet restore $* 12 | @echo. 13 | @echo Aliases: 14 | @echo. 15 | @doskey /MACROS 16 | @CD Build 17 | %comspec% -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Richard Brown 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /ZipDeploy.TestApp2_1/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Reflection; 3 | using System.Runtime.Versioning; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace ZipDeploy.TestApp2_1 7 | { 8 | public class HomeController : Controller 9 | { 10 | private const int c_version = 123; 11 | 12 | public IActionResult Index() 13 | { 14 | return Content($"Version={c_version}", "text/html"); 15 | } 16 | 17 | public IActionResult Runtime() 18 | { 19 | var runtimeVersion = GetNetCoreVersion(); 20 | runtimeVersion = string.Join(".", runtimeVersion.Split('.').Take(2)); 21 | return Content(runtimeVersion); 22 | } 23 | 24 | // https://weblog.west-wind.com/posts/2018/Apr/12/Getting-the-NET-Core-Runtime-Version-in-a-Running-Application 25 | public static string GetNetCoreVersion() 26 | { 27 | var fullVersion = Assembly 28 | .GetEntryAssembly()? 29 | .GetCustomAttribute()? 30 | .FrameworkName; 31 | 32 | var versionNumber = fullVersion.Split('=')[1]; 33 | return versionNumber.TrimStart('v'); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp2_1/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | using NLog; 4 | using NLog.Common; 5 | using NLog.Config; 6 | using NLog.Targets; 7 | using NLog.Web; 8 | 9 | namespace ZipDeploy.TestApp2_1 10 | { 11 | public static class Program 12 | { 13 | public static void Main(string[] args) 14 | { 15 | InternalLogger.LogFile = "logs\\nlog.internal.log"; 16 | InternalLogger.LogLevel = LogLevel.Info; 17 | 18 | LogManager.ThrowExceptions = true; 19 | var config = new LoggingConfiguration(); 20 | var layout = "${longdate}|${level:uppercase=true}|${processid}|${logger}|${message} ${exception:format=tostring}"; 21 | var logFile = new FileTarget("fileTarget") { FileName = "logs\\nlog.log", Layout = layout }; 22 | config.AddTarget(logFile); 23 | config.AddRule(LogLevel.Trace, LogLevel.Fatal, logFile); 24 | 25 | LogManager.ThrowExceptions = true; 26 | LogManager.Configuration = config; 27 | 28 | BuildWebHost(args).Run(); 29 | } 30 | 31 | public static IWebHost BuildWebHost(string[] args) => 32 | WebHost.CreateDefaultBuilder(args) 33 | .UseNLog() 34 | .UseIISIntegration() 35 | .UseStartup() 36 | .Build(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp2_1/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:59694/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Mvc": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:59696/" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /ZipDeploy.TestApp2_1/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace ZipDeploy.TestApp2_1 9 | { 10 | public class Startup 11 | { 12 | public Startup(IConfiguration configuration, ILoggerFactory loggerFactory) 13 | { 14 | Configuration = configuration; 15 | LoggerFactory = loggerFactory; 16 | } 17 | 18 | public IConfiguration Configuration { get; } 19 | public ILoggerFactory LoggerFactory { get; } 20 | 21 | // This method gets called by the runtime. Use this method to add services to the container. 22 | public void ConfigureServices(IServiceCollection services) 23 | { 24 | LoggerFactory.CreateLogger().LogInformation("Startup ConfigureServices"); 25 | services.AddZipDeploy(o => o.IgnorePathStarting("logs").UsingProcessLock(GetType().FullName, TimeSpan.FromSeconds(20))); 26 | services.AddMvc(); 27 | } 28 | 29 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 30 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILogger logger) 31 | { 32 | logger.LogInformation("Startup Configure"); 33 | app.UseDeveloperExceptionPage(); 34 | 35 | app.UseStaticFiles(); 36 | 37 | app.UseMvc(routes => 38 | { 39 | routes.MapRoute( 40 | name: "default", 41 | template: "{controller=Home}/{action=Index}/{id?}"); 42 | }); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp2_1/ZipDeploy.TestApp2_1.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp2_1/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp2_1/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp2_1/bundleconfig.json: -------------------------------------------------------------------------------- 1 | // Configure bundling and minification for the project. 2 | // More info at https://go.microsoft.com/fwlink/?LinkId=808241 3 | [ 4 | { 5 | "outputFileName": "wwwroot/css/site.min.css", 6 | // An array of relative input file paths. Globbing patterns supported 7 | "inputFiles": [ 8 | "wwwroot/css/site.css" 9 | ] 10 | }, 11 | { 12 | "outputFileName": "wwwroot/js/site.min.js", 13 | "inputFiles": [ 14 | "wwwroot/js/site.js" 15 | ], 16 | // Optionally specify minification options 17 | "minify": { 18 | "enabled": true, 19 | "renameLocals": true 20 | }, 21 | // Optionally generate .map file 22 | "sourceMap": false 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp2_1/wwwroot/test.js: -------------------------------------------------------------------------------- 1 | alert(123); -------------------------------------------------------------------------------- /ZipDeploy.TestApp2_1Exe/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Reflection; 3 | using System.Runtime.Versioning; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace ZipDeploy.TestApp2_1Exe 7 | { 8 | public class HomeController : Controller 9 | { 10 | private const int c_version = 123; 11 | 12 | public IActionResult Index() 13 | { 14 | return Content($"Version={c_version}", "text/html"); 15 | } 16 | 17 | public IActionResult Runtime() 18 | { 19 | var runtimeVersion = GetNetCoreVersion(); 20 | runtimeVersion = string.Join(".", runtimeVersion.Split('.').Take(2)); 21 | return Content(runtimeVersion); 22 | } 23 | 24 | // https://weblog.west-wind.com/posts/2018/Apr/12/Getting-the-NET-Core-Runtime-Version-in-a-Running-Application 25 | public static string GetNetCoreVersion() 26 | { 27 | var fullVersion = Assembly 28 | .GetEntryAssembly()? 29 | .GetCustomAttribute()? 30 | .FrameworkName; 31 | 32 | var versionNumber = fullVersion.Split('=')[1]; 33 | return versionNumber.TrimStart('v'); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp2_1Exe/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Logging; 5 | using NLog; 6 | using NLog.Common; 7 | using NLog.Config; 8 | using NLog.Targets; 9 | using NLog.Web; 10 | 11 | namespace ZipDeploy.TestApp2_1Exe 12 | { 13 | public static class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | InternalLogger.LogFile = "logs\\nlog.internal.log"; 18 | InternalLogger.LogLevel = NLog.LogLevel.Info; 19 | 20 | LogManager.ThrowExceptions = true; 21 | var config = new LoggingConfiguration(); 22 | var layout = "${longdate}|${level:uppercase=true}|${processid}|${logger}|${message} ${exception:format=tostring}"; 23 | var logFile = new FileTarget("fileTarget") { FileName = "logs\\nlog.log", Layout = layout }; 24 | config.AddTarget(logFile); 25 | config.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, logFile); 26 | 27 | LogManager.ThrowExceptions = true; 28 | 29 | var loggerFactory = LoggerFactory.Create(c => c 30 | .SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace) 31 | .AddNLog(config)); 32 | 33 | ZipDeploy.Run( 34 | loggerFactory, 35 | options => options.IgnorePathStarting("logs").UsingProcessLock(typeof(Program).FullName, TimeSpan.FromSeconds(20)), 36 | () => BuildWebHost(args).Run()); 37 | } 38 | 39 | public static IWebHost BuildWebHost(string[] args) => 40 | WebHost.CreateDefaultBuilder(args) 41 | .UseNLog() 42 | .UseIISIntegration() 43 | .UseStartup() 44 | .Build(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp2_1Exe/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:59690/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Mvc": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:59692/" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /ZipDeploy.TestApp2_1Exe/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace ZipDeploy.TestApp2_1Exe 7 | { 8 | public class Startup 9 | { 10 | public Startup(IConfiguration configuration) 11 | { 12 | Configuration = configuration; 13 | } 14 | 15 | public IConfiguration Configuration { get; } 16 | 17 | // This method gets called by the runtime. Use this method to add services to the container. 18 | public void ConfigureServices(IServiceCollection services) 19 | { 20 | services.AddMvc(); 21 | } 22 | 23 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 24 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 25 | { 26 | app.UseDeveloperExceptionPage(); 27 | 28 | app.UseStaticFiles(); 29 | 30 | app.UseMvc(routes => 31 | { 32 | routes.MapRoute( 33 | name: "default", 34 | template: "{controller=Home}/{action=Index}/{id?}"); 35 | }); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp2_1Exe/ZipDeploy.TestApp2_1Exe.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp2_1Exe/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp2_1Exe/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp2_1Exe/bundleconfig.json: -------------------------------------------------------------------------------- 1 | // Configure bundling and minification for the project. 2 | // More info at https://go.microsoft.com/fwlink/?LinkId=808241 3 | [ 4 | { 5 | "outputFileName": "wwwroot/css/site.min.css", 6 | // An array of relative input file paths. Globbing patterns supported 7 | "inputFiles": [ 8 | "wwwroot/css/site.css" 9 | ] 10 | }, 11 | { 12 | "outputFileName": "wwwroot/js/site.min.js", 13 | "inputFiles": [ 14 | "wwwroot/js/site.js" 15 | ], 16 | // Optionally specify minification options 17 | "minify": { 18 | "enabled": true, 19 | "renameLocals": true 20 | }, 21 | // Optionally generate .map file 22 | "sourceMap": false 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp2_1Exe/wwwroot/test.js: -------------------------------------------------------------------------------- 1 | alert(123); -------------------------------------------------------------------------------- /ZipDeploy.TestApp3_1/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Reflection; 3 | using System.Runtime.Versioning; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace ZipDeploy.TestApp3_1 7 | { 8 | public class HomeController : Controller 9 | { 10 | private const int c_version = 123; 11 | 12 | public IActionResult Index() 13 | { 14 | return Content($"Version={c_version}", "text/html"); 15 | } 16 | 17 | public IActionResult Runtime() 18 | { 19 | var runtimeVersion = GetNetCoreVersion(); 20 | runtimeVersion = string.Join(".", runtimeVersion.Split('.').Take(2)); 21 | return Content(runtimeVersion); 22 | } 23 | 24 | // https://weblog.west-wind.com/posts/2018/Apr/12/Getting-the-NET-Core-Runtime-Version-in-a-Running-Application 25 | public static string GetNetCoreVersion() 26 | { 27 | var fullVersion = Assembly 28 | .GetEntryAssembly()? 29 | .GetCustomAttribute()? 30 | .FrameworkName; 31 | 32 | var versionNumber = fullVersion.Split('=')[1]; 33 | return versionNumber.TrimStart('v'); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp3_1/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | using NLog; 4 | using NLog.Common; 5 | using NLog.Config; 6 | using NLog.Targets; 7 | using NLog.Web; 8 | 9 | namespace ZipDeploy.TestApp3_1 10 | { 11 | public class Program 12 | { 13 | public static void Main(string[] args) 14 | { 15 | InternalLogger.LogFile = "logs\\nlog.internal.log"; 16 | InternalLogger.LogLevel = LogLevel.Info; 17 | 18 | LogManager.ThrowExceptions = true; 19 | var config = new LoggingConfiguration(); 20 | var layout = "${longdate}|${level:uppercase=true}|${processid}|${logger}|${message} ${exception:format=tostring}"; 21 | var logFile = new FileTarget("fileTarget") { FileName = "logs\\nlog.log", ConcurrentWrites = true, Layout = layout }; 22 | config.AddTarget(logFile); 23 | config.AddRule(LogLevel.Trace, LogLevel.Fatal, logFile); 24 | 25 | LogManager.ThrowExceptions = true; 26 | LogManager.Configuration = config; 27 | 28 | CreateHostBuilder(args).Build().Run(); 29 | } 30 | 31 | public static IHostBuilder CreateHostBuilder(string[] args) => 32 | Host.CreateDefaultBuilder(args) 33 | .UseNLog() 34 | .ConfigureWebHostDefaults(webBuilder => 35 | { 36 | webBuilder.UseStartup(); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp3_1/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:2294", 7 | "sslPort": 44308 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "ZipDeploy.TestApp3_1": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp3_1/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | 7 | namespace ZipDeploy.TestApp3_1 8 | { 9 | public class Startup 10 | { 11 | // This method gets called by the runtime. Use this method to add services to the container. 12 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 13 | public void ConfigureServices(IServiceCollection services) 14 | { 15 | services.AddZipDeploy(o => o.IgnorePathStarting("logs").UsingProcessLock(GetType().FullName, TimeSpan.FromSeconds(20))); 16 | services.AddControllersWithViews(); 17 | } 18 | 19 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 20 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 21 | { 22 | if (env.IsDevelopment()) 23 | { 24 | app.UseDeveloperExceptionPage(); 25 | } 26 | 27 | app.UseStaticFiles(); 28 | app.UseRouting(); 29 | 30 | app.UseEndpoints(endpoints => 31 | { 32 | endpoints.MapControllerRoute( 33 | name: "default", 34 | pattern: "{controller=Home}/{action=Index}/{id?}"); 35 | }); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp3_1/ZipDeploy.TestApp3_1.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp3_1/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp3_1/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /ZipDeploy.TestApp3_1/wwwroot/test.js: -------------------------------------------------------------------------------- 1 | alert(123); -------------------------------------------------------------------------------- /ZipDeploy.Tests/CoverageFilter.txt: -------------------------------------------------------------------------------- 1 | -[ZipDeploy]ZipDeploy.ZipDeploy 2 | -[ZipDeploy.Tests]ZipDeploy.Tests.TestApp.* 3 | -[ZipDeploy.Tests]ZipDeploy.Tests.FileSystem 4 | -------------------------------------------------------------------------------- /ZipDeploy.Tests/DetectionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using FluentAssertions; 5 | using Microsoft.Extensions.Logging; 6 | using NUnit.Framework; 7 | using ZipDeploy.Tests.TestApp; 8 | 9 | namespace ZipDeploy.Tests 10 | { 11 | [TestFixture] 12 | public class DetectionTests 13 | { 14 | private string _originalCurrentDirectory; 15 | private string _filesFolder; 16 | 17 | [SetUp] 18 | public void SetUp() 19 | { 20 | _filesFolder = Path.Combine(Test.GetOutputFolder(), "testFiles"); 21 | FileSystem.DeleteFolder(_filesFolder); 22 | Directory.CreateDirectory(_filesFolder); 23 | _originalCurrentDirectory = Environment.CurrentDirectory; 24 | Environment.CurrentDirectory = _filesFolder; 25 | } 26 | 27 | [TearDown] 28 | public void TearDown() 29 | { 30 | Environment.CurrentDirectory = _originalCurrentDirectory; 31 | } 32 | 33 | [Test] 34 | public async Task WhenPackageDeplyed_PackageIsDetected() 35 | { 36 | var detected = false; 37 | using (var detector = NewDetectPackage()) 38 | { 39 | detector.PackageDetectedAsync += () => { detected = true; return Task.CompletedTask; }; 40 | await detector.StartedAsync(hadStartupErrors: false); 41 | 42 | detected.Should().Be(false); 43 | 44 | File.WriteAllBytes(ZipDeployOptions.DefaultNewPackageFileName, new byte[0]); 45 | 46 | Wait.For(TimeSpan.FromSeconds(1), () => detected.Should().Be(true)); 47 | } 48 | } 49 | 50 | private DetectPackage NewDetectPackage(Action configure = null) 51 | { 52 | var options = new ZipDeployOptions(); 53 | configure?.Invoke(options); 54 | return new DetectPackage(new LoggerFactory().CreateLogger(), options); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ZipDeploy.Tests/FileSystem.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading; 3 | 4 | namespace ZipDeploy.Tests 5 | { 6 | public static class FileSystem 7 | { 8 | public static void CopyDir(string sourceDir, string destDir) 9 | { 10 | DeleteFolder(destDir); 11 | 12 | Directory.CreateDirectory(destDir); 13 | 14 | foreach (var srcDir in Directory.GetDirectories(sourceDir, "*", SearchOption.AllDirectories)) 15 | Directory.CreateDirectory(srcDir.Replace(sourceDir, destDir)); 16 | 17 | foreach (var file in Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories)) 18 | File.Copy(file, file.Replace(sourceDir, destDir)); 19 | } 20 | 21 | public static void CreateFolder(string file) 22 | { 23 | var folder = Path.GetDirectoryName(file); 24 | 25 | if (!string.IsNullOrWhiteSpace(folder) && !Directory.Exists(folder)) 26 | Directory.CreateDirectory(folder); 27 | } 28 | 29 | public static void DeleteFolder(string folder) 30 | { 31 | var count = 3; 32 | 33 | while (Directory.Exists(folder)) 34 | try { Directory.Delete(folder, true); } 35 | catch 36 | { 37 | Thread.Sleep(0); 38 | 39 | if (count-- == 0) 40 | throw; 41 | } 42 | } 43 | 44 | public static void CopySource(string slnFolder, string srcCopyFolder, string projectName) 45 | { 46 | var src = Path.Combine(slnFolder, projectName); 47 | var copy = Path.Combine(srcCopyFolder, projectName); 48 | 49 | CopyDir(src, copy); 50 | } 51 | 52 | public static void ReplaceText(string folder, string file, string find, string replace) 53 | { 54 | var path = Path.Combine(folder, file); 55 | var content = File.ReadAllText(path); 56 | content = content.Replace(find, replace); 57 | File.WriteAllText(path, content); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ZipDeploy.Tests/Test.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using NUnit.Framework; 3 | 4 | namespace ZipDeploy.Tests 5 | { 6 | public static class Test 7 | { 8 | public static void WriteProgress(string line) 9 | { 10 | TestContext.Progress.WriteLine(line); 11 | } 12 | 13 | public static string GetSlnFolder() 14 | { 15 | var slnPath = Path.GetFullPath("."); 16 | 17 | while (!File.Exists(Path.Combine(slnPath, "ZipDeploy.sln"))) 18 | slnPath = Directory.GetParent(slnPath).FullName; 19 | 20 | return slnPath; 21 | } 22 | 23 | public static string GetOutputFolder() 24 | { 25 | var outputFolder = Path.Combine(GetSlnFolder(), "_output"); 26 | Directory.CreateDirectory(outputFolder); 27 | return outputFolder; 28 | } 29 | 30 | public class IsSlowAttribute : CategoryAttribute 31 | { 32 | public IsSlowAttribute() : base("Slow") 33 | { 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ZipDeploy.Tests/TestApp/Exec.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using FluentAssertions; 3 | 4 | namespace ZipDeploy.Tests.TestApp 5 | { 6 | public static class Exec 7 | { 8 | public static void Cmd(string workingDir, string program, string args) 9 | { 10 | using (var process = new Process()) 11 | { 12 | process.StartInfo.FileName = program; 13 | process.StartInfo.Arguments = args; 14 | process.StartInfo.WorkingDirectory = workingDir; 15 | process.StartInfo.UseShellExecute = false; 16 | process.Start(); 17 | process.WaitForExit(); 18 | 19 | process.ExitCode.Should().Be(0); 20 | } 21 | } 22 | 23 | public static void DotnetPublish(string workingDir) 24 | { 25 | Cmd(workingDir, "dotnet.exe", "publish --self-contained --runtime win-x64"); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ZipDeploy.Tests/TestApp/IisAdmin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Security.AccessControl; 7 | using System.Security.Principal; 8 | using Microsoft.Web.Administration; 9 | 10 | namespace ZipDeploy.Tests.TestApp 11 | { 12 | public static class IisAdmin 13 | { 14 | private const string c_iisName = "ZipDeployTestApp"; 15 | private const int c_iisPort = 8099; 16 | 17 | public static void ShowLogOnFail(string iisFolder, Action action) 18 | { 19 | try 20 | { 21 | action(); 22 | } 23 | catch (Exception e) 24 | { 25 | var logsFolder = Path.Combine(iisFolder, "logs"); 26 | var logFiles = new Dictionary(); 27 | 28 | foreach (var logFile in Directory.GetFiles(logsFolder)) 29 | logFiles.Add(logFile, File.ReadAllText(logFile)); 30 | 31 | var log = logFiles.Any() 32 | ? string.Join("\n\n", logFiles.Select(lf => $"{lf.Key}:\n{lf.Value}")) 33 | : $"No log files found in {logsFolder}"; 34 | 35 | throw new Exception($"assertion failure with log:\n\n{log}\n\n", e); 36 | } 37 | } 38 | 39 | public static void VerifyModuleInstalled(string moduleName, string downloadUrl) 40 | { 41 | using (var iisManager = new ServerManager()) 42 | { 43 | var globalModulesList = iisManager.GetApplicationHostConfiguration() 44 | .GetSection("system.webServer/globalModules") 45 | .GetCollection(); 46 | 47 | var globalModules = globalModulesList.Select(m => m.Attributes["name"].Value.ToString()).ToList(); 48 | 49 | if (globalModules.Contains(moduleName)) 50 | return; 51 | 52 | Test.WriteProgress($"Downloading {downloadUrl} for module {moduleName}"); 53 | var filename = Path.GetFileName(downloadUrl); 54 | 55 | if (!File.Exists(filename)) 56 | new WebClient().DownloadFile(downloadUrl, filename); 57 | 58 | Test.WriteProgress($"Executing {filename} /install /quiet /norestart"); 59 | Exec.Cmd("", filename, "/install /quiet /norestart"); 60 | Exec.Cmd("", "iisreset", ""); 61 | } 62 | } 63 | 64 | public static void CreateIisSite(string iisFolder) 65 | { 66 | var sec = new DirectorySecurity(iisFolder, AccessControlSections.Access); 67 | var everyone = new SecurityIdentifier(WellKnownSidType.WorldSid, null); 68 | var rights = FileSystemRights.FullControl; 69 | var inheritFlags = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit; 70 | var propFlags = PropagationFlags.InheritOnly; 71 | sec.AddAccessRule(new FileSystemAccessRule(everyone, rights, inheritFlags, propFlags, AccessControlType.Allow)); 72 | new DirectoryInfo(iisFolder).SetAccessControl(sec); 73 | 74 | using (var iisManager = new ServerManager()) 75 | { 76 | DeleteIisSite(iisManager); 77 | 78 | var pool = iisManager.ApplicationPools.Add(c_iisName); 79 | pool.ProcessModel.IdentityType = ProcessModelIdentityType.NetworkService; 80 | pool.ProcessModel.ShutdownTimeLimit = TimeSpan.FromSeconds(13); // remove this once IIS recycle works on local machine OK 81 | pool.Recycling.DisallowOverlappingRotation = true; 82 | 83 | var site = iisManager.Sites.Add("ZipDeployTestApp", iisFolder, c_iisPort); 84 | site.ApplicationDefaults.ApplicationPoolName = pool.Name; 85 | 86 | iisManager.CommitChanges(); 87 | 88 | Test.WriteProgress($"Created IIS site {c_iisName}:{c_iisPort} in {iisFolder}"); 89 | } 90 | } 91 | 92 | public static void DeleteIisSite() 93 | { 94 | using (var iisManager = new ServerManager()) 95 | { 96 | var siteCount = iisManager.Sites.Count(s => s.Name == c_iisName); 97 | 98 | if (siteCount > 0) 99 | DeleteIisSite(iisManager); 100 | 101 | var poolCount = iisManager.ApplicationPools.Count(p => p.Name == c_iisName); 102 | 103 | if (poolCount > 0) 104 | DeleteIisPool(iisManager); 105 | } 106 | } 107 | 108 | private static void DeleteIisSite(ServerManager iisManager) 109 | { 110 | var site = iisManager.Sites.SingleOrDefault(s => s.Name == c_iisName); 111 | 112 | if (site == null) 113 | return; 114 | 115 | iisManager.Sites.Remove(site); 116 | iisManager.CommitChanges(); 117 | Test.WriteProgress($"Removed IIS site {c_iisName}"); 118 | } 119 | 120 | private static void DeleteIisPool(ServerManager iisManager) 121 | { 122 | var pool = iisManager.ApplicationPools.SingleOrDefault(s => s.Name == c_iisName); 123 | 124 | if (pool == null) 125 | return; 126 | 127 | iisManager.ApplicationPools.Remove(pool); 128 | iisManager.CommitChanges(); 129 | Test.WriteProgress($"Removed IIS pool {c_iisName}"); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /ZipDeploy.Tests/TestApp/IisTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | using System.Net.Http; 4 | using FluentAssertions; 5 | using NUnit.Framework; 6 | 7 | namespace ZipDeploy.Tests.TestApp 8 | { 9 | [TestFixture] 10 | public class IisTests 11 | { 12 | [Test] 13 | [Test.IsSlow] 14 | public void DeployZipExeStyle() 15 | { 16 | IisAdmin.VerifyModuleInstalled( 17 | moduleName: "AspNetCoreModule", 18 | downloadUrl: "https://download.microsoft.com/download/6/E/B/6EBD972D-2E2F-41EB-9668-F73F5FDDC09C/dotnet-hosting-2.1.3-win.exe"); 19 | 20 | DeployZip(new DeployZipOptions 21 | { 22 | ExpectedRuntimeVersion = "2.1", 23 | AppSourceFolder = "ZipDeploy.TestApp2_1Exe", 24 | AppPublishFolder = @"bin\Debug\netcoreapp2.1\win-x64\publish", 25 | }); 26 | } 27 | 28 | [Test] 29 | [Test.IsSlow] 30 | public void DeployZip2_1() 31 | { 32 | IisAdmin.VerifyModuleInstalled( 33 | moduleName: "AspNetCoreModule", 34 | downloadUrl: "https://download.microsoft.com/download/6/E/B/6EBD972D-2E2F-41EB-9668-F73F5FDDC09C/dotnet-hosting-2.1.3-win.exe"); 35 | 36 | DeployZip(new DeployZipOptions 37 | { 38 | ExpectedRuntimeVersion = "2.1", 39 | AppSourceFolder = "ZipDeploy.TestApp2_1", 40 | AppPublishFolder = @"bin\Debug\netcoreapp2.1\win-x64\publish", 41 | }); 42 | } 43 | 44 | [Test] 45 | [Test.IsSlow] 46 | public void DeployZip3_1() 47 | { 48 | IisAdmin.VerifyModuleInstalled( 49 | moduleName: "AspNetCoreModuleV2", 50 | downloadUrl: "https://download.visualstudio.microsoft.com/download/pr/7e35ac45-bb15-450a-946c-fe6ea287f854/a37cfb0987e21097c7969dda482cebd3/dotnet-hosting-3.1.10-win.exe"); 51 | 52 | DeployZip(new DeployZipOptions 53 | { 54 | ExpectedRuntimeVersion = "3.1", 55 | AppSourceFolder = "ZipDeploy.TestApp3_1", 56 | AppPublishFolder = @"bin\Debug\netcoreapp3.1\win-x64\publish", 57 | }); 58 | } 59 | 60 | private class DeployZipOptions 61 | { 62 | public string ExpectedRuntimeVersion; 63 | public string AppSourceFolder; 64 | public string AppPublishFolder; 65 | } 66 | 67 | private void DeployZip(DeployZipOptions options) 68 | { 69 | Test.WriteProgress($"appSourceFolder={options.AppSourceFolder}"); 70 | IisAdmin.DeleteIisSite(); 71 | 72 | var outputFolder = Test.GetOutputFolder(); 73 | Test.WriteProgress($"outputFolder={outputFolder}"); 74 | 75 | var slnFolder = Test.GetSlnFolder(); 76 | Test.WriteProgress($"slnFolder={slnFolder}"); 77 | 78 | var srcCopyFolder = Path.Combine(outputFolder, "src"); 79 | 80 | FileSystem.DeleteFolder(srcCopyFolder); 81 | FileSystem.CopySource(slnFolder, srcCopyFolder, "Build"); 82 | FileSystem.CopySource(slnFolder, srcCopyFolder, "ZipDeploy"); 83 | FileSystem.CopySource(slnFolder, srcCopyFolder, options.AppSourceFolder); 84 | File.Copy(Path.Combine(slnFolder, "icon.png"), Path.Combine(srcCopyFolder, "icon.png")); 85 | 86 | var testAppfolder = Path.Combine(srcCopyFolder, options.AppSourceFolder); 87 | Exec.DotnetPublish(testAppfolder); 88 | 89 | var publishFolder = Path.Combine(testAppfolder, options.AppPublishFolder); 90 | var iisFolder = Path.Combine(outputFolder, "IisSite"); 91 | 92 | FileSystem.DeleteFolder(iisFolder); 93 | Directory.Move(publishFolder, iisFolder); 94 | 95 | var existingZipTemp = Path.Combine(outputFolder, "publish.zip"); 96 | ZipFile.CreateFromDirectory(iisFolder, existingZipTemp); 97 | var existingZip = Path.Combine(iisFolder, "publish.zip"); 98 | File.Move(existingZipTemp, existingZip); 99 | 100 | IisAdmin.ShowLogOnFail(iisFolder, () => 101 | { 102 | IisAdmin.CreateIisSite(iisFolder); 103 | 104 | Get("http://localhost:8099/home/runtime").Should().Be(options.ExpectedRuntimeVersion); 105 | 106 | Wait.For(() => 107 | { 108 | File.Exists(Path.Combine(iisFolder, ZipDeployOptions.DefaultNewPackageFileName)).Should().BeFalse("existing publish.zip should have been picked up at startup"); 109 | File.Exists(Path.Combine(iisFolder, ZipDeployOptions.DefaultDeployedPackageFileName)).Should().BeTrue("deployment should be complete, and publish.zip should have been renamed to deployed.zip"); 110 | }); 111 | 112 | // avoids IIS returning 500.3: https://github.com/dotnet/aspnetcore/issues/10117 113 | System.Threading.Thread.Sleep(200); 114 | 115 | Get("http://localhost:8099").Should().Contain("Version=123"); 116 | Get("http://localhost:8099/test.js").Should().Contain("alert(123);"); 117 | 118 | Test.WriteProgress($"Verified version 123"); 119 | 120 | FileSystem.CopySource(slnFolder, srcCopyFolder, options.AppSourceFolder); 121 | FileSystem.ReplaceText(testAppfolder, @"HomeController.cs", "private const int c_version = 123;", "private const int c_version = 234;"); 122 | FileSystem.ReplaceText(testAppfolder, @"wwwroot\test.js", "alert(123);", "alert(234);"); 123 | Exec.DotnetPublish(testAppfolder); 124 | 125 | var uploadingZip = Path.Combine(iisFolder, "uploading.zip"); 126 | ZipFile.CreateFromDirectory(publishFolder, uploadingZip); 127 | 128 | var configFile = Path.Combine(iisFolder, "web.config"); 129 | var lastConfigChange = File.GetLastWriteTimeUtc(configFile); 130 | 131 | var publishZip = Path.Combine(iisFolder, ZipDeployOptions.DefaultNewPackageFileName); 132 | File.Move(uploadingZip, publishZip); 133 | 134 | Test.WriteProgress($"Wrote {publishZip}"); 135 | 136 | Wait.For(() => 137 | { 138 | File.Exists(publishZip).Should().BeFalse($"file {publishZip} should have been picked up by ZipDeploy"); 139 | File.GetLastWriteTimeUtc(configFile).Should().NotBe(lastConfigChange, $"file {configFile} should have been updated"); 140 | }); 141 | 142 | Test.WriteProgress($"Verified {publishZip} has been picked up and {configFile} has been updated"); 143 | System.Threading.Thread.Sleep(200); // remove once IIS recycle working locally 144 | 145 | var webConfig = Path.Combine(iisFolder, "web.config"); 146 | File.WriteAllText(webConfig, File.ReadAllText(webConfig).Replace("stdoutLogEnabled=\"false\"", "stdoutLogEnabled=\"true\"")); 147 | 148 | // the binaries have been replaced, and the web.config should have been touched 149 | // the next request should complete the installation, and return the new responses 150 | 151 | Get("http://localhost:8099").Should().Contain("Version=234"); 152 | Get("http://localhost:8099/test.js").Should().Contain("alert(234);"); 153 | 154 | Test.WriteProgress($"Verified version 234"); 155 | 156 | File.Exists(Path.Combine(iisFolder, ZipDeployOptions.DefaultNewPackageFileName)).Should().BeFalse("publish.zip should have been renamed to deployed.zip"); 157 | File.Exists(Path.Combine(iisFolder, ZipDeployOptions.DefaultDeployedPackageFileName)).Should().BeTrue("deployment should be complete, and publish.zip should have been renamed to deployed.zip"); 158 | File.Exists(Path.Combine(iisFolder, "zzz__ZipDeploy.dll.fordelete.txt")).Should().BeFalse("obsolete binaries should have been deleted on next startup"); 159 | 160 | IisAdmin.DeleteIisSite(); 161 | }); 162 | 163 | Test.WriteProgress($"appSourceFolder={options.AppSourceFolder} success"); 164 | } 165 | 166 | private string Get(string url) 167 | { 168 | var response = new HttpClient().GetAsync(url).GetAwaiter().GetResult(); 169 | using (var stream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()) 170 | using (var streamReader = new StreamReader(stream)) 171 | return streamReader.ReadToEnd(); 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /ZipDeploy.Tests/TestApp/Wait.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace ZipDeploy.Tests.TestApp 5 | { 6 | public static class Wait 7 | { 8 | private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30); 9 | private static TimeSpan Timeout = DefaultTimeout; 10 | 11 | public static void For(Action action) 12 | { 13 | For(Timeout, action); 14 | } 15 | 16 | public static void For(TimeSpan timeout, Action action) 17 | { 18 | For(Timeout, () => { action(); return true; }); 19 | } 20 | 21 | public static T For(Func query) 22 | { 23 | return For(Timeout, query); 24 | } 25 | 26 | public static T For(TimeSpan timeout, Func query) 27 | { 28 | var until = DateTime.Now + timeout; 29 | 30 | return WaitUntil(until, query); 31 | } 32 | 33 | public static void Until(string reason, Func condition) 34 | { 35 | Until(Timeout, reason, condition); 36 | } 37 | 38 | public static void Until(TimeSpan timeout, string reason, Func condition) 39 | { 40 | var until = DateTime.Now + timeout; 41 | 42 | while (!condition() && DateTime.Now < until) 43 | Thread.Sleep(20); 44 | 45 | if (DateTime.Now > until) 46 | throw new Exception("Timeout waiting for: " + reason); 47 | } 48 | 49 | private static T WaitUntil(DateTime until, Func query) 50 | { 51 | while (true) 52 | { 53 | try 54 | { 55 | return query(); 56 | } 57 | catch 58 | { 59 | if (DateTime.Now > until) 60 | throw; 61 | 62 | Thread.Sleep(20); 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ZipDeploy.Tests/UnzipperTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.Threading.Tasks; 5 | using FluentAssertions; 6 | using Microsoft.Extensions.Logging; 7 | using NUnit.Framework; 8 | 9 | namespace ZipDeploy.Tests 10 | { 11 | [TestFixture] 12 | public class UnzipperTests 13 | { 14 | private string _originalCurrentDirectory; 15 | private string _filesFolder; 16 | 17 | [SetUp] 18 | public void SetUp() 19 | { 20 | _filesFolder = Path.Combine(Test.GetOutputFolder(), "testFiles"); 21 | FileSystem.DeleteFolder(_filesFolder); 22 | Directory.CreateDirectory(_filesFolder); 23 | _originalCurrentDirectory = Environment.CurrentDirectory; 24 | Environment.CurrentDirectory = _filesFolder; 25 | } 26 | 27 | [TearDown] 28 | public void TearDown() 29 | { 30 | Environment.CurrentDirectory = _originalCurrentDirectory; 31 | } 32 | 33 | [Test] 34 | public async Task Unzip_BinariesAreRenamed() 35 | { 36 | ExistingFiles("binary1.dll", "binary2.exe"); 37 | 38 | await CreateZipAsync(ZipDeployOptions.DefaultNewPackageFileName, "binary1.dll", "binary2.exe"); 39 | 40 | await NewUnzipper().UnzipAsync(); 41 | 42 | File.ReadAllText("binary1.dll").Should().Be("zipped content of binary1.dll"); 43 | File.ReadAllText("zzz__binary1.dll.fordelete.txt").Should().Be("existing content of binary1.dll"); 44 | File.ReadAllText("binary2.exe").Should().Be("zipped content of binary2.exe"); 45 | File.ReadAllText("zzz__binary2.exe.fordelete.txt").Should().Be("existing content of binary2.exe"); 46 | } 47 | 48 | [Test] 49 | public async Task Unzip_ExistingMarkedDeleteFilesAreOverwritten() 50 | { 51 | var expectedContent = "existing content of binary.dll"; 52 | 53 | ExistingFiles("binary.dll", "zzz__binary.dll.fordelete.txt"); 54 | 55 | File.ReadAllText("zzz__binary.dll.fordelete.txt").Should().NotBe(expectedContent); 56 | 57 | await CreateZipAsync(ZipDeployOptions.DefaultNewPackageFileName, @"binary.dll"); 58 | 59 | await NewUnzipper().UnzipAsync(); 60 | 61 | File.ReadAllText("zzz__binary.dll.fordelete.txt").Should().Be(expectedContent); 62 | } 63 | 64 | [Test] 65 | public async Task Unzip_OverwritesWebConfig() 66 | { 67 | ExistingFiles("web.config"); 68 | 69 | await CreateZipAsync(ZipDeployOptions.DefaultNewPackageFileName, @"web.config"); 70 | 71 | var restarter = new AspNetRestart(new LoggerFactory().CreateLogger(), new ProcessWebConfig(), new CanPauseTrigger(), new ZipDeployOptions()); 72 | await restarter.TriggerAsync(); 73 | 74 | File.ReadAllText("web.config").Should().Be("zipped content of web.config"); 75 | } 76 | 77 | [Test] 78 | public async Task Unzip_KeepsOriginalIfNoChanges() 79 | { 80 | ExistingFiles(); 81 | 82 | await CreateZipAsync(ZipDeployOptions.DefaultNewPackageFileName, @"binary.dll", @"nonbinary.txt"); 83 | 84 | var unzipper = NewUnzipper(); 85 | await unzipper.UnzipAsync(); 86 | 87 | var existingModifiedDateTime = DateTime.UtcNow - TimeSpan.FromHours(3); 88 | File.SetLastWriteTimeUtc("binary.dll", existingModifiedDateTime); 89 | File.SetLastWriteTimeUtc("nonbinary.txt", existingModifiedDateTime); 90 | 91 | await CreateZipAsync(ZipDeployOptions.DefaultNewPackageFileName, @"binary.dll", @"nonbinary.txt"); 92 | 93 | await unzipper.UnzipAsync(); 94 | 95 | File.GetLastWriteTimeUtc("binary.dll").Should().Be(existingModifiedDateTime); 96 | File.GetLastWriteTimeUtc("nonbinary.txt").Should().Be(existingModifiedDateTime); 97 | } 98 | 99 | [Test] 100 | public async Task Unzip_OverwritesExistingDeployedArchive() 101 | { 102 | ExistingFiles(ZipDeployOptions.DefaultDeployedPackageFileName); 103 | 104 | await CreateZipAsync(ZipDeployOptions.DefaultNewPackageFileName); 105 | 106 | await NewUnzipper().UnzipAsync(); 107 | 108 | File.Exists(ZipDeployOptions.DefaultNewPackageFileName).Should().BeFalse("publish.zip should have been renamed"); 109 | File.Exists(ZipDeployOptions.DefaultDeployedPackageFileName).Should().BeTrue("publish.zip should have been renamed to deployed.zip"); 110 | File.ReadAllText(ZipDeployOptions.DefaultDeployedPackageFileName).Should().NotBe("existing content of deployed.zip", "previous deployed.zip should have been overwritten"); 111 | } 112 | 113 | [Test] 114 | public async Task Unzip_RenamesObsoleteBinaries() 115 | { 116 | ExistingFiles("file1.dll", "legacy.dll"); 117 | 118 | await CreateZipAsync(ZipDeployOptions.DefaultNewPackageFileName, "file1.dll"); 119 | 120 | await NewUnzipper().UnzipAsync(); 121 | 122 | File.ReadAllText("file1.dll").Should().Be("zipped content of file1.dll"); 123 | File.Exists("legacy.dll").Should().BeFalse("obsolete legacy.dll should have been renamed"); 124 | } 125 | 126 | [Test] 127 | public async Task Unzip_ExtractsMissingFilesWithHash() 128 | { 129 | await CreateZipAsync(ZipDeployOptions.DefaultNewPackageFileName, "file1.dll"); 130 | 131 | await NewUnzipper().UnzipAsync(); 132 | 133 | File.Delete("file1.dll"); 134 | File.Move(ZipDeployOptions.DefaultDeployedPackageFileName, ZipDeployOptions.DefaultNewPackageFileName); 135 | 136 | await NewUnzipper().UnzipAsync(); 137 | 138 | File.Exists("file1.dll").Should().BeTrue(); 139 | } 140 | 141 | [Test] 142 | public async Task Sync_ObsoleteFilesAreRemoved() 143 | { 144 | ExistingFiles( 145 | "new.dll", 146 | "zzz__obsolete.dll.fordelete.txt", 147 | @"wwwroot\zzz__new.txt.fordelete.txt"); 148 | 149 | await CreateZipAsync(ZipDeployOptions.DefaultDeployedPackageFileName, "new.dll", @"wwwroot\new.txt"); 150 | 151 | await NewCleaner().DeleteObsoleteFilesAsync(); 152 | 153 | File.Exists("zzz__obsolete.dll.fordelete.txt").Should().BeFalse("ZipDeploy should have deleted obsolete.dll.fordelete.txt"); 154 | File.Exists(@"wwwroot\zzz__new.txt.fordelete.txt").Should().BeFalse("new.txt.fordelete.txt should have been removed"); 155 | } 156 | 157 | [Test] 158 | public async Task Sync_ObsoleteFoldersAreRemoved() 159 | { 160 | ExistingFiles( 161 | @"wwwroot\zzz__old.txt.fordelete.txt\tmp.txt"); 162 | 163 | await NewCleaner().DeleteObsoleteFilesAsync(); 164 | 165 | Directory.Exists("wwwroot\\zzz__old.txt.fordelete.txt").Should().BeFalse("Cleaner should have removed obsolete folder"); 166 | } 167 | 168 | [Test] 169 | public async Task Unzip_NonBinariesAreExtracted() 170 | { 171 | ExistingFiles(@"wwwroot\file1.txt"); 172 | 173 | await CreateZipAsync(ZipDeployOptions.DefaultNewPackageFileName, 174 | @"file1.dll", 175 | @"wwwroot\file1.txt", 176 | @"wwwroot\file2.txt"); 177 | 178 | await NewUnzipper().UnzipAsync(); 179 | 180 | File.ReadAllText(@"wwwroot\file1.txt").Should().Be(@"zipped content of wwwroot\file1.txt"); 181 | File.ReadAllText(@"wwwroot\file2.txt").Should().Be(@"zipped content of wwwroot\file2.txt"); 182 | } 183 | 184 | [Test] 185 | public async Task Unzip_CanUnzipFileWithSameNameAsDirectory() 186 | { 187 | ExistingFiles(@"wwwroot\licence\licence"); 188 | 189 | await CreateZipAsync(ZipDeployOptions.DefaultNewPackageFileName, 190 | @"wwwroot\licence"); 191 | 192 | await NewUnzipper().UnzipAsync(); 193 | 194 | File.ReadAllText(@"wwwroot\licence").Should().Be(@"zipped content of wwwroot\licence"); 195 | } 196 | 197 | [Test] 198 | public async Task Unzip_CanUnzipFileToDirectoryWithSameNameAsFile() 199 | { 200 | ExistingFiles(@"wwwroot\licence"); 201 | 202 | await CreateZipAsync(ZipDeployOptions.DefaultNewPackageFileName, 203 | @"wwwroot\licence\licence"); 204 | 205 | await NewUnzipper().UnzipAsync(); 206 | 207 | File.ReadAllText(@"wwwroot\licence\licence").Should().Be(@"zipped content of wwwroot\licence\licence"); 208 | } 209 | 210 | [Test] 211 | public async Task CaseInsensitiveFilesAreHandled() 212 | { 213 | ExistingFiles("file.txt", "file.dll"); 214 | 215 | await CreateZipAsync(ZipDeployOptions.DefaultNewPackageFileName, "FILE.TXT", "FILE.DLL"); 216 | 217 | var unzipper = NewUnzipper(); 218 | await unzipper.UnzipAsync(); 219 | 220 | File.ReadAllText("FILE.TXT").Should().Be("zipped content of FILE.TXT"); 221 | File.ReadAllText("FILE.DLL").Should().Be("zipped content of FILE.DLL"); 222 | } 223 | 224 | [Test] 225 | public async Task PathsCanBeExcluded() 226 | { 227 | ExistingFiles( 228 | "log.txt", 229 | "uploads/sub/sub/file1.txt", 230 | "uploads//sub/file2.dll", 231 | "uploads2\\subfolder\\file3.txt"); 232 | 233 | await CreateZipAsync(ZipDeployOptions.DefaultNewPackageFileName, "file1.txt"); 234 | 235 | var unzipper = NewUnzipper(opt => opt 236 | .IgnorePathStarting("log.txt") 237 | .IgnorePathStarting("uploads\\sub") 238 | .IgnorePathStarting("uploads2\\subfolder")); 239 | 240 | await unzipper.UnzipAsync(); 241 | 242 | File.Exists("file1.txt").Should().BeTrue("unzip should have extracted file1.txt"); 243 | File.Exists("log.txt").Should().BeTrue("log.txt should have been ignored"); 244 | File.Exists("uploads/sub/sub/file1.txt").Should().BeTrue("uploads/sub/file1.txt should have been ignored"); 245 | File.Exists("uploads/sub/file2.dll").Should().BeTrue("uploads/file2.dll should have been ignored"); 246 | File.Exists("uploads2/subfolder/file3.txt").Should().BeTrue("uploads2/subfolder/file3.txt should have been ignored"); 247 | } 248 | 249 | [Test] 250 | public void ZipDeploy_RegistrationIsComplete() 251 | { 252 | ZipDeploy.Run( 253 | new LoggerFactory(), 254 | _ => { }, 255 | () => 256 | { 257 | // do nothing 258 | }); 259 | } 260 | 261 | private Unzipper NewUnzipper(Action configure = null) 262 | { 263 | var options = new ZipDeployOptions(); 264 | 265 | configure?.Invoke(options); 266 | return new Unzipper(new LoggerFactory().CreateLogger(), options); 267 | } 268 | 269 | private Cleaner NewCleaner() 270 | { 271 | return new Cleaner(new LoggerFactory().CreateLogger()); 272 | } 273 | 274 | private void ExistingFiles(params string[] files) 275 | { 276 | foreach (var file in files) 277 | { 278 | FileSystem.CreateFolder(file); 279 | File.WriteAllText(file, $"existing content of {file}"); 280 | } 281 | } 282 | 283 | private async Task CreateZipAsync(string zipFileName, params string[] files) 284 | { 285 | var tmpFile = zipFileName + ".tmp"; 286 | 287 | using (var zipStream = File.OpenWrite(tmpFile)) 288 | using (var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Create)) 289 | { 290 | foreach (var file in files) 291 | { 292 | var entry = zipArchive.CreateEntry(file); 293 | using (var entryStream = entry.Open()) 294 | using (var streamWriter = new StreamWriter(entryStream)) 295 | await streamWriter.WriteAsync($"zipped content of {file}"); 296 | } 297 | } 298 | 299 | File.Move(tmpFile, zipFileName); 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /ZipDeploy.Tests/ZipDeploy.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | net461 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /ZipDeploy.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 16 3 | VisualStudioVersion = 16.0.29806.167 4 | MinimumVisualStudioVersion = 15.0.26124.0 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZipDeploy", "ZipDeploy\ZipDeploy.csproj", "{C97D2221-483F-4C0C-A96F-621226C1494C}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZipDeploy.TestApp2_1Exe", "ZipDeploy.TestApp2_1Exe\ZipDeploy.TestApp2_1Exe.csproj", "{8BE4FD2D-793C-475D-816D-16F444241D3F}" 8 | EndProject 9 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZipDeploy.Tests", "ZipDeploy.Tests\ZipDeploy.Tests.csproj", "{958122A5-2C20-4E61-A99A-48A9EC5BA7BB}" 10 | EndProject 11 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "root", "root", "{DC38B5AE-6742-4A3E-816D-E597FC6083B4}" 12 | ProjectSection(SolutionItems) = preProject 13 | .gitignore = .gitignore 14 | appveyor.yml = appveyor.yml 15 | CommandPrompt.bat = CommandPrompt.bat 16 | global.json = global.json 17 | LICENSE.txt = LICENSE.txt 18 | readme.md = readme.md 19 | EndProjectSection 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Build", "Build\Build.csproj", "{8BB56072-A8E0-42C7-9FA7-A12D168680D4}" 22 | EndProject 23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZipDeploy.TestApp2_1", "ZipDeploy.TestApp2_1\ZipDeploy.TestApp2_1.csproj", "{3F3554BF-382D-422C-A8B1-9165C5E9DA77}" 24 | EndProject 25 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZipDeploy.TestApp3_1", "ZipDeploy.TestApp3_1\ZipDeploy.TestApp3_1.csproj", "{1E16A2E0-9D0F-4B9C-A11D-D648A919C188}" 26 | EndProject 27 | Global 28 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 29 | Debug|Any CPU = Debug|Any CPU 30 | Debug|x64 = Debug|x64 31 | Debug|x86 = Debug|x86 32 | Release|Any CPU = Release|Any CPU 33 | Release|x64 = Release|x64 34 | Release|x86 = Release|x86 35 | EndGlobalSection 36 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 37 | {C97D2221-483F-4C0C-A96F-621226C1494C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {C97D2221-483F-4C0C-A96F-621226C1494C}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {C97D2221-483F-4C0C-A96F-621226C1494C}.Debug|x64.ActiveCfg = Debug|Any CPU 40 | {C97D2221-483F-4C0C-A96F-621226C1494C}.Debug|x64.Build.0 = Debug|Any CPU 41 | {C97D2221-483F-4C0C-A96F-621226C1494C}.Debug|x86.ActiveCfg = Debug|Any CPU 42 | {C97D2221-483F-4C0C-A96F-621226C1494C}.Debug|x86.Build.0 = Debug|Any CPU 43 | {C97D2221-483F-4C0C-A96F-621226C1494C}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {C97D2221-483F-4C0C-A96F-621226C1494C}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {C97D2221-483F-4C0C-A96F-621226C1494C}.Release|x64.ActiveCfg = Release|Any CPU 46 | {C97D2221-483F-4C0C-A96F-621226C1494C}.Release|x64.Build.0 = Release|Any CPU 47 | {C97D2221-483F-4C0C-A96F-621226C1494C}.Release|x86.ActiveCfg = Release|Any CPU 48 | {C97D2221-483F-4C0C-A96F-621226C1494C}.Release|x86.Build.0 = Release|Any CPU 49 | {8BE4FD2D-793C-475D-816D-16F444241D3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {8BE4FD2D-793C-475D-816D-16F444241D3F}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {8BE4FD2D-793C-475D-816D-16F444241D3F}.Debug|x64.ActiveCfg = Debug|Any CPU 52 | {8BE4FD2D-793C-475D-816D-16F444241D3F}.Debug|x64.Build.0 = Debug|Any CPU 53 | {8BE4FD2D-793C-475D-816D-16F444241D3F}.Debug|x86.ActiveCfg = Debug|Any CPU 54 | {8BE4FD2D-793C-475D-816D-16F444241D3F}.Debug|x86.Build.0 = Debug|Any CPU 55 | {8BE4FD2D-793C-475D-816D-16F444241D3F}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {8BE4FD2D-793C-475D-816D-16F444241D3F}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {8BE4FD2D-793C-475D-816D-16F444241D3F}.Release|x64.ActiveCfg = Release|Any CPU 58 | {8BE4FD2D-793C-475D-816D-16F444241D3F}.Release|x64.Build.0 = Release|Any CPU 59 | {8BE4FD2D-793C-475D-816D-16F444241D3F}.Release|x86.ActiveCfg = Release|Any CPU 60 | {8BE4FD2D-793C-475D-816D-16F444241D3F}.Release|x86.Build.0 = Release|Any CPU 61 | {958122A5-2C20-4E61-A99A-48A9EC5BA7BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 62 | {958122A5-2C20-4E61-A99A-48A9EC5BA7BB}.Debug|Any CPU.Build.0 = Debug|Any CPU 63 | {958122A5-2C20-4E61-A99A-48A9EC5BA7BB}.Debug|x64.ActiveCfg = Debug|Any CPU 64 | {958122A5-2C20-4E61-A99A-48A9EC5BA7BB}.Debug|x64.Build.0 = Debug|Any CPU 65 | {958122A5-2C20-4E61-A99A-48A9EC5BA7BB}.Debug|x86.ActiveCfg = Debug|Any CPU 66 | {958122A5-2C20-4E61-A99A-48A9EC5BA7BB}.Debug|x86.Build.0 = Debug|Any CPU 67 | {958122A5-2C20-4E61-A99A-48A9EC5BA7BB}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {958122A5-2C20-4E61-A99A-48A9EC5BA7BB}.Release|Any CPU.Build.0 = Release|Any CPU 69 | {958122A5-2C20-4E61-A99A-48A9EC5BA7BB}.Release|x64.ActiveCfg = Release|Any CPU 70 | {958122A5-2C20-4E61-A99A-48A9EC5BA7BB}.Release|x64.Build.0 = Release|Any CPU 71 | {958122A5-2C20-4E61-A99A-48A9EC5BA7BB}.Release|x86.ActiveCfg = Release|Any CPU 72 | {958122A5-2C20-4E61-A99A-48A9EC5BA7BB}.Release|x86.Build.0 = Release|Any CPU 73 | {8BB56072-A8E0-42C7-9FA7-A12D168680D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 74 | {8BB56072-A8E0-42C7-9FA7-A12D168680D4}.Debug|Any CPU.Build.0 = Debug|Any CPU 75 | {8BB56072-A8E0-42C7-9FA7-A12D168680D4}.Debug|x64.ActiveCfg = Debug|Any CPU 76 | {8BB56072-A8E0-42C7-9FA7-A12D168680D4}.Debug|x64.Build.0 = Debug|Any CPU 77 | {8BB56072-A8E0-42C7-9FA7-A12D168680D4}.Debug|x86.ActiveCfg = Debug|Any CPU 78 | {8BB56072-A8E0-42C7-9FA7-A12D168680D4}.Debug|x86.Build.0 = Debug|Any CPU 79 | {8BB56072-A8E0-42C7-9FA7-A12D168680D4}.Release|Any CPU.ActiveCfg = Release|Any CPU 80 | {8BB56072-A8E0-42C7-9FA7-A12D168680D4}.Release|Any CPU.Build.0 = Release|Any CPU 81 | {8BB56072-A8E0-42C7-9FA7-A12D168680D4}.Release|x64.ActiveCfg = Release|Any CPU 82 | {8BB56072-A8E0-42C7-9FA7-A12D168680D4}.Release|x64.Build.0 = Release|Any CPU 83 | {8BB56072-A8E0-42C7-9FA7-A12D168680D4}.Release|x86.ActiveCfg = Release|Any CPU 84 | {8BB56072-A8E0-42C7-9FA7-A12D168680D4}.Release|x86.Build.0 = Release|Any CPU 85 | {3F3554BF-382D-422C-A8B1-9165C5E9DA77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 86 | {3F3554BF-382D-422C-A8B1-9165C5E9DA77}.Debug|Any CPU.Build.0 = Debug|Any CPU 87 | {3F3554BF-382D-422C-A8B1-9165C5E9DA77}.Debug|x64.ActiveCfg = Debug|Any CPU 88 | {3F3554BF-382D-422C-A8B1-9165C5E9DA77}.Debug|x64.Build.0 = Debug|Any CPU 89 | {3F3554BF-382D-422C-A8B1-9165C5E9DA77}.Debug|x86.ActiveCfg = Debug|Any CPU 90 | {3F3554BF-382D-422C-A8B1-9165C5E9DA77}.Debug|x86.Build.0 = Debug|Any CPU 91 | {3F3554BF-382D-422C-A8B1-9165C5E9DA77}.Release|Any CPU.ActiveCfg = Release|Any CPU 92 | {3F3554BF-382D-422C-A8B1-9165C5E9DA77}.Release|Any CPU.Build.0 = Release|Any CPU 93 | {3F3554BF-382D-422C-A8B1-9165C5E9DA77}.Release|x64.ActiveCfg = Release|Any CPU 94 | {3F3554BF-382D-422C-A8B1-9165C5E9DA77}.Release|x64.Build.0 = Release|Any CPU 95 | {3F3554BF-382D-422C-A8B1-9165C5E9DA77}.Release|x86.ActiveCfg = Release|Any CPU 96 | {3F3554BF-382D-422C-A8B1-9165C5E9DA77}.Release|x86.Build.0 = Release|Any CPU 97 | {1E16A2E0-9D0F-4B9C-A11D-D648A919C188}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 98 | {1E16A2E0-9D0F-4B9C-A11D-D648A919C188}.Debug|Any CPU.Build.0 = Debug|Any CPU 99 | {1E16A2E0-9D0F-4B9C-A11D-D648A919C188}.Debug|x64.ActiveCfg = Debug|Any CPU 100 | {1E16A2E0-9D0F-4B9C-A11D-D648A919C188}.Debug|x64.Build.0 = Debug|Any CPU 101 | {1E16A2E0-9D0F-4B9C-A11D-D648A919C188}.Debug|x86.ActiveCfg = Debug|Any CPU 102 | {1E16A2E0-9D0F-4B9C-A11D-D648A919C188}.Debug|x86.Build.0 = Debug|Any CPU 103 | {1E16A2E0-9D0F-4B9C-A11D-D648A919C188}.Release|Any CPU.ActiveCfg = Release|Any CPU 104 | {1E16A2E0-9D0F-4B9C-A11D-D648A919C188}.Release|Any CPU.Build.0 = Release|Any CPU 105 | {1E16A2E0-9D0F-4B9C-A11D-D648A919C188}.Release|x64.ActiveCfg = Release|Any CPU 106 | {1E16A2E0-9D0F-4B9C-A11D-D648A919C188}.Release|x64.Build.0 = Release|Any CPU 107 | {1E16A2E0-9D0F-4B9C-A11D-D648A919C188}.Release|x86.ActiveCfg = Release|Any CPU 108 | {1E16A2E0-9D0F-4B9C-A11D-D648A919C188}.Release|x86.Build.0 = Release|Any CPU 109 | EndGlobalSection 110 | GlobalSection(SolutionProperties) = preSolution 111 | HideSolutionNode = FALSE 112 | EndGlobalSection 113 | GlobalSection(ExtensibilityGlobals) = postSolution 114 | SolutionGuid = {AF247C94-C602-445F-9D9F-A544FE31DC38} 115 | EndGlobalSection 116 | EndGlobal 117 | -------------------------------------------------------------------------------- /ZipDeploy/Application.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace ZipDeploy 9 | { 10 | public class Application : IHostedService 11 | { 12 | private readonly ILogger _logger; 13 | private readonly ILockProcess _lockProcess; 14 | private readonly ICleaner _cleaner; 15 | private readonly IDetectPackage _detectPackage; 16 | private readonly ITriggerRestart _triggerRestart; 17 | private readonly ZipDeployOptions _options; 18 | private readonly IUnzipper _unzipper; 19 | 20 | public Application( 21 | ILogger logger, 22 | ILockProcess lockProcess, 23 | ICleaner cleaner, 24 | IDetectPackage detectPackage, 25 | ITriggerRestart triggerRestart, 26 | ZipDeployOptions options, 27 | IUnzipper unzipper) 28 | { 29 | _logger = logger; 30 | _lockProcess = lockProcess; 31 | _cleaner = cleaner; 32 | _detectPackage = detectPackage; 33 | _triggerRestart = triggerRestart; 34 | _options = options; 35 | _unzipper = unzipper; 36 | } 37 | 38 | async Task IHostedService.StartAsync(CancellationToken cancellationToken) 39 | { 40 | var hadStartupErrors = false; 41 | _logger.LogInformation("Application startup"); 42 | await _lockProcess.LockAsync(); 43 | 44 | _logger.LogDebug("ZipDeploy wireup package detection"); 45 | _detectPackage.PackageDetectedAsync += _triggerRestart.TriggerAsync; 46 | 47 | var deleteObsoleteFilesResult = 48 | await _logger.RetryAsync(_options, "Delete obsolete files", () => 49 | _cleaner.DeleteObsoleteFilesAsync()); 50 | 51 | hadStartupErrors = hadStartupErrors || deleteObsoleteFilesResult == RetryResult.Failure; 52 | 53 | await _logger.RetryAsync(_options, "Start package detection", () => 54 | _detectPackage.StartedAsync(hadStartupErrors)); 55 | } 56 | 57 | async Task IHostedService.StopAsync(CancellationToken cancellationToken) 58 | { 59 | _logger.LogInformation("Application stopped"); 60 | _detectPackage.Stop(); 61 | 62 | await _logger.RetryAsync(_options, "ZipDeploy before shutdown", async () => 63 | { 64 | if (File.Exists(Path.Combine(Environment.CurrentDirectory, _options.NewPackageFileName))) 65 | { 66 | _logger.LogDebug("Found package {packageName}", _options.NewPackageFileName); 67 | await _unzipper.UnzipAsync(); 68 | } 69 | 70 | _logger.LogDebug("ZipDeploy completed after process shutdown"); 71 | }); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ZipDeploy/CanPauseTrigger.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | 3 | namespace ZipDeploy 4 | { 5 | public interface ICanPauseTrigger 6 | { 7 | void Release(SemaphoreSlim semaphore); 8 | } 9 | 10 | public class CanPauseTrigger : ICanPauseTrigger 11 | { 12 | public virtual void Release(SemaphoreSlim semaphore) 13 | { 14 | semaphore.Release(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ZipDeploy/Cleaner.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace ZipDeploy 7 | { 8 | public interface ICleaner 9 | { 10 | Task DeleteObsoleteFilesAsync(); 11 | } 12 | 13 | public class Cleaner : ICleaner 14 | { 15 | private ILogger _logger; 16 | private FsUtil _fsUtil; 17 | 18 | public Cleaner(ILogger logger) 19 | { 20 | _logger = logger; 21 | _fsUtil = new FsUtil(logger); 22 | } 23 | 24 | public virtual Task DeleteObsoleteFilesAsync() 25 | { 26 | _logger.LogInformation("Deleting obsoleted files and directories"); 27 | var obsoleteFileCount = 0; 28 | var obsoleteDirectoryCount = 0; 29 | 30 | foreach (var fullName in Directory.GetFiles(".", "*", SearchOption.AllDirectories).Select(f => _fsUtil.NormalisePath(f))) 31 | if (_fsUtil.IsForDelete(fullName)) 32 | { 33 | _fsUtil.DeleteFile(fullName); 34 | obsoleteFileCount++; 35 | } 36 | 37 | foreach (var fullName in Directory.GetDirectories(".", "*", SearchOption.AllDirectories).Select(f => _fsUtil.NormalisePath(f))) 38 | if (_fsUtil.IsForDelete(fullName)) 39 | { 40 | _fsUtil.DeleteDirectory(fullName); 41 | obsoleteDirectoryCount++; 42 | } 43 | 44 | _logger.LogInformation("Deleted {obsoleteFileCount} obsolete files and {obsoleteDirectoryCount} obsolete directories", obsoleteFileCount, obsoleteDirectoryCount); 45 | return Task.CompletedTask; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ZipDeploy/DetectPackage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace ZipDeploy 7 | { 8 | /// Raises PackageDetected when changes 9 | public interface IDetectPackage 10 | { 11 | event Func PackageDetectedAsync; 12 | Task StartedAsync(bool hadStartupErrors); 13 | void Stop(); 14 | } 15 | 16 | public class DetectPackage : IDetectPackage, IDisposable 17 | { 18 | private ILogger _logger; 19 | private ZipDeployOptions _options; 20 | private FileSystemWatcher _fsw; 21 | 22 | public event Func PackageDetectedAsync; 23 | 24 | public DetectPackage(ILogger logger, ZipDeployOptions options) 25 | { 26 | _logger = logger; 27 | _options = options; 28 | } 29 | 30 | public virtual Task StartedAsync(bool hadStartupErrors) 31 | { 32 | _fsw = new FileSystemWatcher(Environment.CurrentDirectory, _options.NewPackageFileName); 33 | _fsw.Created += OnPackageDetected; 34 | _fsw.Changed += OnPackageDetected; 35 | _fsw.Renamed += OnPackageDetected; 36 | _fsw.Error += OnError; 37 | _fsw.EnableRaisingEvents = true; 38 | 39 | var restart = false; 40 | var restartReason = string.Empty; 41 | 42 | if (hadStartupErrors && _options.RestartOnStartupError) 43 | { 44 | restartReason = $"Error during startup"; 45 | restart = true; 46 | } 47 | else if (File.Exists(_options.NewPackageFileName)) 48 | { 49 | restartReason = $"Found {_options.NewPackageFileName} at startup"; 50 | restart = true; 51 | } 52 | 53 | if (restart) 54 | { 55 | _logger.LogInformation($"{restartReason} - waiting {_options.StartupPublishDelay} to trigger restart"); 56 | 57 | // don't wait on this task - it should run in the background, and trigger after the appropriate period 58 | Task.Run(async () => 59 | { 60 | await Task.Delay(_options.StartupPublishDelay); 61 | await OnPackageDetectedAsync(this, null, restartReason); 62 | }); 63 | } 64 | else 65 | { 66 | _logger.LogInformation($"Watching for {_options.NewPackageFileName} in {Environment.CurrentDirectory}"); 67 | } 68 | 69 | return Task.CompletedTask; 70 | } 71 | 72 | public void Dispose() 73 | { 74 | Stop(); 75 | } 76 | 77 | public void Stop() 78 | { 79 | using (_fsw) 80 | { 81 | if (_fsw != null) 82 | _fsw.EnableRaisingEvents = false; 83 | 84 | _fsw = null; 85 | } 86 | } 87 | 88 | private void OnError(object sender, ErrorEventArgs e) 89 | { 90 | var ex = e?.GetException(); 91 | _logger.LogError(ex, $"Error in FileSystemWatcher: {ex?.Message}"); 92 | } 93 | 94 | protected virtual void OnPackageDetected(object sender, FileSystemEventArgs e) 95 | { 96 | OnPackageDetectedAsync(sender, e, "Detected installation package").GetAwaiter().GetResult(); 97 | } 98 | 99 | protected virtual async Task OnPackageDetectedAsync(object sender, FileSystemEventArgs e, string reason) 100 | { 101 | _logger.LogInformation($"OnPackageDetectedAsync: {reason}"); 102 | 103 | await _logger.RetryAsync(_options, "zip file detected", async () => 104 | { 105 | if (PackageDetectedAsync != null) 106 | await PackageDetectedAsync(); 107 | 108 | Stop(); 109 | }); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /ZipDeploy/FsUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace ZipDeploy 7 | { 8 | public class FsUtil 9 | { 10 | public const string ForDeletePrefix = "zzz__"; 11 | public const string ForDeletePostfix = ".fordelete.txt"; 12 | 13 | private ILogger _logger; 14 | 15 | public FsUtil(ILogger logger) 16 | { 17 | _logger = logger; 18 | } 19 | 20 | public string NormalisePath(string file) 21 | { 22 | file = file.Replace("\\", "/"); 23 | file = file.StartsWith("./") ? file.Substring(2) : file; 24 | return file; 25 | } 26 | 27 | public void DeleteFile(string file) 28 | { 29 | Try(() => File.Delete(file), 30 | () => File.Exists(file), 31 | $"delete file {file}"); 32 | } 33 | 34 | public void MoveFile(string file, string destinationFile) 35 | { 36 | Try(() => File.Move(file, destinationFile), 37 | () => File.Exists(file), 38 | $"moving file {file} to {destinationFile}"); 39 | } 40 | 41 | public void DeleteDirectory(string directory) 42 | { 43 | Try(() => Directory.Delete(directory, true), 44 | () => Directory.Exists(directory), 45 | $"delete directory {directory}"); 46 | } 47 | 48 | public void MoveDirectory(string directory, string destinationDirectory) 49 | { 50 | Try(() => Directory.Move(directory, destinationDirectory), 51 | () => Directory.Exists(directory), 52 | $"moving directory {directory} to {destinationDirectory}"); 53 | } 54 | 55 | public void PrepareForDelete(string fullName) 56 | { 57 | var fileExists = File.Exists(fullName); 58 | var directoryExists = !fileExists && Directory.Exists(fullName); 59 | 60 | if (!fileExists && !directoryExists) 61 | { 62 | var parent = Path.GetDirectoryName(fullName); 63 | 64 | while (!string.IsNullOrWhiteSpace(parent)) 65 | { 66 | if (File.Exists(parent)) 67 | PrepareForDelete(parent); 68 | 69 | parent = Path.GetDirectoryName(parent); 70 | } 71 | 72 | return; 73 | } 74 | 75 | var fileName = Path.GetFileName(fullName); 76 | var path = Path.GetDirectoryName(fullName); 77 | var destinationFullName = Path.Combine(path, $"{ForDeletePrefix}{fileName}{ForDeletePostfix}"); 78 | 79 | PrepareForDelete(destinationFullName); 80 | 81 | if (fileExists) 82 | MoveFile(fullName, destinationFullName); 83 | else 84 | MoveDirectory(fullName, destinationFullName); 85 | } 86 | 87 | public bool IsForDelete(string fullName) 88 | { 89 | return Path.GetFileName(fullName).StartsWith(ForDeletePrefix) && fullName.EndsWith(ForDeletePostfix); 90 | } 91 | 92 | private void Try(Action action, Func notComplete, string what) 93 | { 94 | var count = 3; 95 | 96 | while (notComplete()) 97 | { 98 | try 99 | { 100 | _logger.LogDebug(what); 101 | action(); 102 | } 103 | catch (Exception e) 104 | { 105 | _logger.LogDebug(e, $"Error during {what}"); 106 | Thread.Sleep(0); 107 | 108 | if (count-- <= 0) 109 | throw; 110 | } 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /ZipDeploy/LockProcess.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.InteropServices; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace ZipDeploy 9 | { 10 | public interface ILockProcess 11 | { 12 | Task LockAsync(); 13 | } 14 | 15 | public class LockProcess : ILockProcess 16 | { 17 | private Semaphore _semaphore; 18 | private ILogger _logger; 19 | private ZipDeployOptions _options; 20 | private ITriggerRestart _triggerRestart; 21 | 22 | public LockProcess(ILogger logger, ZipDeployOptions options, ITriggerRestart triggerRestart) 23 | { 24 | _logger = logger; 25 | _options = options; 26 | _triggerRestart = triggerRestart; 27 | } 28 | 29 | public virtual async Task LockAsync() 30 | { 31 | if (string.IsNullOrWhiteSpace(_options.ProcessLockName)) 32 | { 33 | _logger.LogInformation("No global lock configured"); 34 | return; 35 | } 36 | 37 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 38 | { 39 | _logger.LogInformation("Global lock only supported on Windows"); 40 | return; 41 | } 42 | 43 | var semaphoreName = $"Global\\{_options.ProcessLockName}"; 44 | 45 | var logMessage = _options.ProcessLockTimeout.HasValue 46 | ? $"Waiting on lock {semaphoreName} for {_options.ProcessLockTimeout}" 47 | : $"Waiting on lock {semaphoreName}"; 48 | 49 | _logger.LogDebug(logMessage); 50 | 51 | try 52 | { 53 | var locked = false; 54 | var stopwatch = Stopwatch.StartNew(); 55 | 56 | while (!locked) 57 | { 58 | #pragma warning disable PC001 // API not supported on all platforms 59 | _semaphore = new Semaphore(1, 1, semaphoreName, out locked); 60 | #pragma warning restore PC001 // API not supported on all platforms 61 | 62 | if (!locked) 63 | { 64 | // if we couldn't create a new (global) named semaphore 65 | // then another process has it, so we should dispose of this one and wait 66 | 67 | using (_semaphore) { } 68 | await Task.Delay(200); 69 | 70 | if (_options.ProcessLockTimeout.HasValue && stopwatch.Elapsed > _options.ProcessLockTimeout.Value) 71 | throw new Exception($"Could not create new Semaphore {semaphoreName}"); 72 | } 73 | } 74 | } 75 | catch(Exception ex) 76 | { 77 | await TryTriggerRestartAsync(); 78 | throw new Exception($"Could not obtain lock {semaphoreName}", ex); 79 | } 80 | 81 | // note, we deliberately don't dispose of the sempahore we create 82 | // so that Windows will clear it up once the process dies (and so dll files should be unlocked by this point) 83 | _logger.LogInformation($"Obtained lock {semaphoreName}"); 84 | } 85 | 86 | private async Task TryTriggerRestartAsync() 87 | { 88 | try 89 | { 90 | _logger.LogInformation($"Attempting to trigger restart after failing to obtain lock"); 91 | await _triggerRestart.TriggerAsync(); 92 | } 93 | catch (Exception ex) 94 | { 95 | _logger.LogError(ex, $"Error attepting restart: {ex.Message}"); 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /ZipDeploy/LoggerExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace ZipDeploy 6 | { 7 | public enum RetryResult 8 | { 9 | Success, 10 | Failure, 11 | } 12 | 13 | public static class LoggerExtensions 14 | { 15 | /// 16 | /// Returns true if there was an error 17 | /// 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// 23 | public static async Task RetryAsync(this ILogger logger, ZipDeployOptions options, string description, Func actionAsync) 24 | { 25 | const int maxRetries = 3; 26 | var retryCount = 0; 27 | 28 | while (retryCount < maxRetries) 29 | { 30 | try 31 | { 32 | logger.LogDebug("Start {description}", description); 33 | await actionAsync(); 34 | logger.LogDebug("Finish {description}", description); 35 | return RetryResult.Success; 36 | } 37 | catch (Exception ex) 38 | { 39 | retryCount++; 40 | var level = retryCount < maxRetries ? LogLevel.Warning : LogLevel.Error; 41 | logger.Log(level, ex, "Error {retryCount} during {description}: {error}", retryCount, description, ex?.Message); 42 | await Task.Delay(options.ErrorRetryPeriod); 43 | } 44 | } 45 | 46 | return RetryResult.Failure; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ZipDeploy/ProcessWebConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace ZipDeploy 4 | { 5 | public interface IProcessWebConfig 6 | { 7 | Task ProcessAsync(byte[] zippedConfig); 8 | } 9 | 10 | public class ProcessWebConfig : IProcessWebConfig 11 | { 12 | public virtual Task ProcessAsync(byte[] zippedConfig) 13 | { 14 | return Task.FromResult(zippedConfig); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ZipDeploy/Registration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace ZipDeploy 5 | { 6 | public static class Registration 7 | { 8 | public static IServiceCollection RegisterLogger(this IServiceCollection services, ILoggerFactory loggerFactory) 9 | { 10 | services.AddSingleton(loggerFactory); 11 | services.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); 12 | return services; 13 | } 14 | 15 | public static IServiceCollection RegisterDefaults(this IServiceCollection services, ZipDeployOptions options) 16 | { 17 | services.AddSingleton(options); 18 | services.AddSingleton(); 19 | services.AddSingleton(); 20 | services.AddSingleton(); 21 | services.AddSingleton(); 22 | services.AddSingleton(); 23 | services.AddSingleton(); 24 | services.AddSingleton(); 25 | return services; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ZipDeploy/TriggerRestart.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace ZipDeploy 8 | { 9 | public interface ITriggerRestart 10 | { 11 | Task TriggerAsync(); 12 | } 13 | 14 | public class AspNetRestart : ITriggerRestart 15 | { 16 | private ILogger _logger; 17 | private IProcessWebConfig _processWebConfig; 18 | private ICanPauseTrigger _canPauseTrigger; 19 | private ZipDeployOptions _options; 20 | 21 | public AspNetRestart( 22 | ILogger logger, 23 | IProcessWebConfig processWebConfig, 24 | ICanPauseTrigger canPauseTrigger, 25 | ZipDeployOptions options) 26 | { 27 | _logger = logger; 28 | _processWebConfig = processWebConfig; 29 | _canPauseTrigger = canPauseTrigger; 30 | _options = options; 31 | } 32 | 33 | public virtual async Task TriggerAsync() 34 | { 35 | _logger.LogInformation("Awaiting trigger of restart"); 36 | 37 | using (var semaphore = new SemaphoreSlim(0, 1)) 38 | { 39 | _canPauseTrigger.Release(semaphore); 40 | await semaphore.WaitAsync(); 41 | } 42 | 43 | _logger.LogInformation("Triggering restart"); 44 | 45 | await _options.UsingArchiveAsync(_logger, async zipArchive => 46 | { 47 | byte[] webConfigContent = null; 48 | 49 | if (zipArchive == null) 50 | _logger.LogWarning($"Triggering restart when no zip archive detected"); 51 | 52 | var webConfigEntry = zipArchive?.GetEntry("web.config"); 53 | 54 | if (webConfigEntry != null && webConfigEntry.Length != 0) 55 | { 56 | _logger.LogDebug("Found web.config content in package"); 57 | 58 | using (var zipFileContext = webConfigEntry.Open()) 59 | using (var ms = new MemoryStream()) 60 | { 61 | await zipFileContext.CopyToAsync(ms); 62 | webConfigContent = ms.ToArray(); 63 | } 64 | } 65 | 66 | if (webConfigContent == null && File.Exists("web.config")) 67 | { 68 | _logger.LogDebug("Using existing web.config content"); 69 | webConfigContent = File.ReadAllBytes("web.config"); 70 | } 71 | 72 | if (webConfigContent == null) 73 | { 74 | _logger.LogError("Unable to find content for web.config to trigger restart"); 75 | return; 76 | } 77 | 78 | _logger.LogDebug("Triggering restart by touching web.config"); 79 | webConfigContent = await _processWebConfig.ProcessAsync(webConfigContent); 80 | File.WriteAllBytes("web.config", webConfigContent); 81 | File.SetLastWriteTimeUtc("web.config", File.GetLastWriteTimeUtc("web.config") + TimeSpan.FromSeconds(1)); 82 | _options.PathsToIgnore.Add("web.config"); 83 | }); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /ZipDeploy/Unzipper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using System.Linq; 6 | using System.Security.Cryptography; 7 | using System.Threading.Tasks; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace ZipDeploy 11 | { 12 | public interface IUnzipper 13 | { 14 | Task UnzipAsync(); 15 | } 16 | 17 | public class Unzipper : IUnzipper 18 | { 19 | private ILogger _logger; 20 | private ZipDeployOptions _options; 21 | private FsUtil _fsUtil; 22 | 23 | public Unzipper(ILogger logger, ZipDeployOptions options) 24 | { 25 | _logger = logger; 26 | _options = options; 27 | _fsUtil = new FsUtil(logger); 28 | } 29 | 30 | public virtual async Task UnzipAsync() 31 | { 32 | _logger.LogInformation("Unzipping deployment package"); 33 | var zippedFiles = new List(); 34 | 35 | await UsingArchiveAsync(async (entries, fileHashes) => 36 | { 37 | foreach (var entry in entries) 38 | { 39 | var fullName = entry.Key; 40 | zippedFiles.Add(_fsUtil.NormalisePath(fullName)); 41 | await ExtractAsync(fullName, entry.Value, fileHashes); 42 | } 43 | }); 44 | 45 | RenameObsoleteFiles(zippedFiles); 46 | 47 | _fsUtil.PrepareForDelete(_options.DeployedPackageFileName); 48 | _fsUtil.MoveFile(_options.NewPackageFileName, _options.DeployedPackageFileName); 49 | _logger.LogInformation("Completed unzipping of deployment package"); 50 | } 51 | 52 | protected virtual async Task ExtractAsync(string fullName, ZipArchiveEntry zipEntry, IDictionary fileHashes) 53 | { 54 | using (var zipInput = zipEntry.Open()) 55 | using (var md5 = MD5.Create()) 56 | { 57 | var fileHashBytes = md5.ComputeHash(zipInput); 58 | var fileHash = BitConverter.ToString(fileHashBytes); 59 | 60 | if (fileHashes.ContainsKey(fullName) && fileHash == fileHashes[fullName] && File.Exists(fullName)) 61 | { 62 | _logger.LogDebug($"no changes detected - skipping {fullName}"); 63 | return; 64 | } 65 | 66 | fileHashes[fullName] = fileHash; 67 | } 68 | 69 | _fsUtil.PrepareForDelete(fullName); 70 | 71 | var folder = Path.GetDirectoryName(fullName); 72 | 73 | if (!string.IsNullOrWhiteSpace(folder)) 74 | Directory.CreateDirectory(folder); 75 | 76 | using (var streamWriter = File.Create(fullName)) 77 | using (var zipInput = zipEntry.Open()) 78 | { 79 | _logger.LogDebug($"extracting {fullName}"); 80 | await zipInput.CopyToAsync(streamWriter); 81 | } 82 | } 83 | 84 | protected virtual async Task UsingArchiveAsync(Func, IDictionary, Task> actionAsync) 85 | { 86 | await _options.UsingArchiveAsync(_logger, async zipArchive => 87 | { 88 | var entries = zipArchive.Entries 89 | .Where(e => e.Length != 0) 90 | .ToDictionary(zfe => zfe.FullName, zfe => zfe); 91 | 92 | _logger.LogDebug($"{entries.Count} files in zip"); 93 | 94 | var fileHashes = new Dictionary(); 95 | 96 | if (File.Exists(_options.HashesFileName)) 97 | { 98 | fileHashes = File.ReadAllLines(_options.HashesFileName) 99 | .Select(l => l.Split('|')) 100 | .ToDictionary(a => a[0], a => a[1]); 101 | } 102 | 103 | await actionAsync(entries, fileHashes); 104 | 105 | var hashesStrings = fileHashes 106 | .Select(kvp => $"{kvp.Key}|{kvp.Value}"); 107 | 108 | File.WriteAllLines(_options.HashesFileName, hashesStrings); 109 | }); 110 | } 111 | 112 | protected virtual void RenameObsoleteFiles(IList zippedFiles) 113 | { 114 | foreach (var fullName in Directory.GetFiles(".", "*", SearchOption.AllDirectories).Select(f => _fsUtil.NormalisePath(f))) 115 | if (!zippedFiles.Contains(fullName) && !ShouldIgnore(fullName)) 116 | _fsUtil.PrepareForDelete(fullName); 117 | } 118 | 119 | protected virtual bool ShouldIgnore(string forDeleteFile) 120 | { 121 | var file = Path.GetFileName(forDeleteFile); 122 | 123 | var knownFiles = new List 124 | { 125 | _options.NewPackageFileName, 126 | _options.DeployedPackageFileName, 127 | _options.HashesFileName, 128 | }; 129 | 130 | var isKnownfile = knownFiles.Contains(file); 131 | 132 | if (isKnownfile) 133 | return true; 134 | 135 | if (_fsUtil.IsForDelete(forDeleteFile)) 136 | return true; 137 | 138 | if (_options.PathsToIgnore.Any(p => _fsUtil.NormalisePath(forDeleteFile).ToLower().StartsWith(_fsUtil.NormalisePath(p.ToLower())))) 139 | return true; 140 | 141 | return false; 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /ZipDeploy/ZipDeploy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace ZipDeploy 8 | { 9 | public static class ZipDeploy 10 | { 11 | public static void Run(Action program) 12 | { 13 | Run(null, null, program); 14 | } 15 | 16 | public static void Run(Action setupOptions, Action program) 17 | { 18 | Run(null, setupOptions, program); 19 | } 20 | 21 | public static void Run(ILoggerFactory loggerFactory, Action setupOptions, Action program) 22 | { 23 | Func sync = () => { program(); return Task.CompletedTask; }; 24 | RunAsync(loggerFactory, setupOptions, sync).GetAwaiter().GetResult(); 25 | } 26 | 27 | public static async Task RunAsync(Func programAsync) 28 | { 29 | await RunAsync(null, null, programAsync); 30 | } 31 | 32 | public static async Task RunAsync(Action setupOptions, Func programAsync) 33 | { 34 | await RunAsync(null, setupOptions, programAsync); 35 | } 36 | 37 | public static async Task RunAsync(ILoggerFactory loggerFactory, Action setupOptions, Func programAsync) 38 | { 39 | loggerFactory = loggerFactory ?? new LoggerFactory(); 40 | var logger = loggerFactory.CreateLogger(typeof(ZipDeploy)); 41 | logger.LogDebug("ZipDeploy starting"); 42 | 43 | var options = new ZipDeployOptions(); 44 | setupOptions?.Invoke(options); 45 | 46 | var provider = new ServiceCollection() 47 | .RegisterLogger(loggerFactory) 48 | .RegisterDefaults(options) 49 | .BuildServiceProvider(); 50 | 51 | using (provider) 52 | { 53 | try 54 | { 55 | var lockProcess = provider.GetRequiredService(); 56 | await lockProcess.LockAsync(); 57 | 58 | var detectPackage = provider.GetRequiredService(); 59 | var triggerRestart = provider.GetRequiredService(); 60 | detectPackage.PackageDetectedAsync += triggerRestart.TriggerAsync; 61 | 62 | var cleaner = provider.GetRequiredService(); 63 | await cleaner.DeleteObsoleteFilesAsync(); 64 | await detectPackage.StartedAsync(hadStartupErrors: false); 65 | await programAsync(); 66 | } 67 | finally 68 | { 69 | provider.GetRequiredService().Stop(); 70 | 71 | await logger.RetryAsync(options, "ZipDeploy before shutdown", async () => 72 | { 73 | if (File.Exists(Path.Combine(Environment.CurrentDirectory, options.NewPackageFileName))) 74 | { 75 | logger.LogInformation("Found package {packageName}", options.NewPackageFileName); 76 | var unzipper = provider.GetRequiredService(); 77 | await unzipper.UnzipAsync(); 78 | } 79 | 80 | logger.LogDebug("ZipDeploy completed after process shutdown"); 81 | }); 82 | } 83 | } 84 | } 85 | 86 | public static IServiceCollection AddZipDeploy(this IServiceCollection services, Action setupOptions = null) 87 | { 88 | var options = new ZipDeployOptions(); 89 | setupOptions?.Invoke(options); 90 | services.RegisterDefaults(options); 91 | services.AddHostedService(); 92 | return services; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /ZipDeploy/ZipDeploy.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | netstandard2.0 7 | true 8 | true 9 | 10 | Full 11 | 12 | 13 | 14 | 15 | 16 | all 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ZipDeploy/ZipDeployOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace ZipDeploy 9 | { 10 | public class ZipDeployOptions 11 | { 12 | public const string DefaultNewPackageFileName = "publish.zip"; 13 | public const string DefaultDeployedPackageFileName = "deployed.zip"; 14 | public const string DefaultHashesFileName = "zipDeployFileHashes.txt"; 15 | 16 | public IList PathsToIgnore { get; protected set; } = new List(); 17 | 18 | public string NewPackageFileName { get; set; } = DefaultNewPackageFileName; 19 | public string DeployedPackageFileName { get; set; } = DefaultDeployedPackageFileName; 20 | public string HashesFileName { get; set; } = DefaultHashesFileName; 21 | public TimeSpan StartupPublishDelay { get; set; } = TimeSpan.FromSeconds(3); 22 | public TimeSpan ErrorRetryPeriod { get; set; } = TimeSpan.FromMilliseconds(500); 23 | public string ProcessLockName { get; set; } 24 | public TimeSpan? ProcessLockTimeout { get; set; } 25 | public bool RestartOnStartupError { get; set; } = true; 26 | 27 | /// Specify any paths to ignore (e.g., "log.txt", or "logs/", or "uploads\today") 28 | public ZipDeployOptions IgnorePathStarting(string path) 29 | { 30 | PathsToIgnore.Add(path); 31 | return this; 32 | } 33 | 34 | /// Specify a named synchronization primitive to use (will be prefixed with Global\ and used to prevent multiple processes running at the same time). 35 | public ZipDeployOptions UsingProcessLock(string name, TimeSpan? timeout = null) 36 | { 37 | ProcessLockName = name; 38 | ProcessLockTimeout = timeout; 39 | return this; 40 | } 41 | 42 | internal async Task UsingArchiveAsync(ILogger logger, Func actionAsync) 43 | { 44 | if (File.Exists(NewPackageFileName)) 45 | { 46 | logger.LogDebug($"Opening {NewPackageFileName}"); 47 | 48 | using (var zipArchive = ZipFile.OpenRead(NewPackageFileName)) 49 | await actionAsync(zipArchive); 50 | } 51 | else 52 | { 53 | logger.LogInformation($"{NewPackageFileName} not found"); 54 | await actionAsync(null); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2019 2 | 3 | services: 4 | - iis 5 | 6 | install: 7 | - choco install dotnet-aspnetcoremodule-v1 8 | - choco install dotnet-aspnetcoremodule-v2 9 | 10 | build_script: 11 | - dotnet --version 12 | - dotnet --info 13 | - cd Build 14 | - dotnet restore Build.csproj 15 | - dotnet msbuild Build.csproj 16 | 17 | # build already runs tests and coverage, so turn AppVeyor tests off 18 | test: off 19 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | plugins: 3 | - jekyll-sitemap 4 | -------------------------------------------------------------------------------- /docs/googlebf97859327ca037d.html: -------------------------------------------------------------------------------- 1 | google-site-verification: googlebf97859327ca037d.html -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 |

ZipDeploy docs

3 | 4 | When an ASP.NET Core site is running, it locks the assemblies that are in use. This prevents the old ASP.NET style x-copy deployment. 5 | 6 | ZipDeploy is a small library to allow you to zip up your publish folder and deploy it by FTP-ing the resulting zip up to a running site. This can prevent errors like ERROR_FILE_IN_USE, or "locked by an external process": 7 | 11 | 12 | 16 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 |

Quickstart

2 | 3 | Execute the following: 4 | 5 | dotnet new mvc 6 | dotnet add package ZipDeploy 7 | 8 | Open `Startup.cs`, and add: 9 | 10 | using ZipDeploy; 11 | 12 | ... 13 | 14 | public void ConfigureServices(IServiceCollection services) 15 | { 16 | services.AddZipDeploy(); 17 | 18 | Now you can do: 19 | 20 | dotnet publish 21 | 22 | ... zip up the generated `publish` folder, and FTP it up to the root of your running site. 23 | 24 | A more detailed walkthrough is covered here: Walkthrough 25 | -------------------------------------------------------------------------------- /docs/walkthrough.md: -------------------------------------------------------------------------------- 1 |

Walkthrough

2 | 3 | This walkthrough will talk you through getting an example of ZipDeploy up and running on your machine using IIS, 7za.exe, and FTP. 4 | 5 | For this walkthrough, you will need: 6 | * IIS 7 | * FTP Server 8 | * .NET Core Windows Server Hosting bundle 9 | * `7za.exe` 10 | * `ncftpput.exe` 11 | 12 | IIS and FTP server can be enabled in Windows from the "Turn Windows features on or off" dialog. ".NET Core Windows Server Hosting bundle" needs to be downloaded and installed to add the AspNetCoreModule to IIS. 13 | `7za.exe` and `ncftpput.exe` are freely available downloads; the walkthrough assumes you have them in your PATH. 14 | 15 | Checkout your ASP.NET Core MVC application to `C:\Temp\MyApp`, and add the ZipDeploy package: 16 | 17 | CD C:\Temp\MyApp 18 | dotnet add package ZipDeploy 19 | 20 | Open `Startup.cs`, and add: 21 | 22 | public void ConfigureServices(IServiceCollection services) 23 | { 24 | services.AddZipDeploy(); 25 | 26 | Package the application: 27 | 28 | dotnet publish 29 | move bin\Debug\net5.0\publish ..\MySite 30 | 31 | Modify the security of the `C:\Temp\MySite` folder to add Everyone with Full Control (purely for demo purposes). 32 | 33 | In IIS Manager, add a website with 'Site name' MySite, 'Physical Path' `C:\Temp\MySite`, and 'port' 8123. Browse to the URL `http://localhost:8123/` to confirm your site is up and running. 34 | 35 | In IIS Manager, add an FTP site with 'FTP site name' MyFtpSite and 'Physical path' `C:\Temp\MySite`, select no SSL, and enable anoymous authentication. In the FTP site Authorization Rules, allow Anonymous Users read and write permissions. Verify the FTP site is working using `ftp localhost` from a command prompt, logging in with username `anonymous` and a blank password, and typing `ls` to see the list of files in the website. 36 | 37 | Make a change to your code that you wish to see reflected after deployment, and package the application again: 38 | 39 | CD C:\Temp\MyApp 40 | dotnet publish 41 | 42 | Re-verify your site is running in the browser (and verify your changes are not present yet). Now zip the contents of the pubish directory, and FTP the resulting zip into the root of the site. 43 | 44 | 7za.exe a publish.zip .\bin\Debug\net5.0\publish\* 45 | ncftpput.exe -S .tmp localhost . publish.zip 46 | 47 | Note two things: 48 | * The zip file should be rooted at the same point as the site (note the relative folder and trailing backslash to `7za.exe`). For example, the `web.config` should be directly inside the zip file, not inside a folder inside the zip file; 49 | * The upload should use a temporary filename until the upload is complete. The option `-S .tmp` means the file is uploaded as `publish.zip.tmp`, then renamed to `publish.zip` once the upload is complete. 50 | 51 | ZipDeploy should detect the presence of the zip file. It will rename the current binaries (which is allowed even when they are in use), and unzip the new ones. 52 | 53 | ZipDeploy then updates the web.config, which makes IIS recycle the ASP.NET Core process. 54 | 55 | The next request to IIS will start the new ASP.NET Core process, at which point ZipDeploy will delete the renamed binaries, and unzip any remaining content. 56 | 57 | Refresh the browser to verify the changes have been reflected, and notice that ZipDeploy has renamed the zip file to `deployed.zip`. 58 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { "version": "3.1.202" } 3 | } -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlukeFan/ZipDeploy/28484c9597ae3ba54220c75ef26438166ecf6357/icon.png -------------------------------------------------------------------------------- /lib/NuGet/NuGet.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlukeFan/ZipDeploy/28484c9597ae3ba54220c75ef26438166ecf6357/lib/NuGet/NuGet.exe -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | [![Build Status](https://ci.appveyor.com/api/projects/status/github/FlukeFan/ZipDeploy?svg=true)](https://ci.appveyor.com/project/FlukeFan/ZipDeploy) 3 | 4 | [![NuGet Badge](https://buildstats.info/nuget/zipdeploy)](https://www.nuget.org/packages/zipdeploy/) 5 | 6 | [Documentation](https://flukefan.github.io/ZipDeploy/) 7 | 8 |
 9 | 
10 | 
11 | ZipDeploy
12 | =========
13 | 
14 | Deploy updates to a running Asp.Net Core IIS application by uploading a zip file.
15 | 
16 | Building
17 | ========
18 | 
19 | To build, open CommandPrompt.bat, and type 'b'.
20 | 
21 | Build commands:
22 | 
23 | br                                      Restore dependencies (execute this first)
24 | b                                       Dev-build
25 | ba                                      Build all (including slow tests)
26 | bw                                      Watch dev-build
27 | bt [test]                               Run tests with filter Name~[test]
28 | btw [test]                              Watch run tests with filter Name~[test]
29 | bc                                      Clean the build outputs
30 | b /t:setApiKey /p:apiKey=[key]          Set the NuGet API key
31 | b /t:push                               Push packages to NuGet and publish them (setApiKey before running this)
32 | 


--------------------------------------------------------------------------------