Skip to content

Commit fcd59f6

Browse files
Merge pull request #6779 from donker/upgrades-ui
UI for management of upgrades
2 parents 3530827 + 66a6d8c commit fcd59f6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2195
-204
lines changed

DNN Platform/Library/DotNetNuke.Library.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,7 @@
863863
<Compile Include="Services\Installer\Installers\UrlProviderInstaller.cs" />
864864
<Compile Include="Services\Installer\LocalUpgradeInfo.cs" />
865865
<Compile Include="Services\Installer\LocalUpgradeService.cs" />
866+
<Compile Include="Services\Installer\LocalUpgradeSettings.cs" />
866867
<Compile Include="Services\Installer\Packages\IPackageController.cs" />
867868
<Compile Include="Services\Installer\Packages\PackageDependencyInfo.cs" />
868869
<Compile Include="Services\Installer\Packages\PackageTypeMemberNameFixer.cs" />

DNN Platform/Library/Services/Installer/ILocalUpgradeService.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
namespace DotNetNuke.Services.Installer;
66

77
using System.Collections.Generic;
8+
using System.IO;
89
using System.Threading;
910
using System.Threading.Tasks;
1011

12+
using DotNetNuke.Abstractions.Application;
13+
1114
/// <summary>Provides the ability to manage upgrades of DNN from local upgrade package files.</summary>
1215
public interface ILocalUpgradeService
1316
{
@@ -16,9 +19,48 @@ public interface ILocalUpgradeService
1619
/// <returns>A task which resolves to a list of <see cref="LocalUpgradeInfo"/> instances.</returns>
1720
Task<IReadOnlyList<LocalUpgradeInfo>> GetLocalUpgrades(CancellationToken cancellationToken);
1821

22+
/// <summary>
23+
/// Retrieves information about a local upgrade from the specified file.
24+
/// </summary>
25+
/// <param name="file">The path to the DNN installation file. Cannot be null or empty.</param>
26+
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
27+
/// <returns>A task that represents the asynchronous operation. The task result contains a <see cref="LocalUpgradeInfo"/>
28+
/// object with details about the upgrade.</returns>
29+
Task<LocalUpgradeInfo> GetLocalUpgradeInfo(string file, CancellationToken cancellationToken);
30+
31+
/// <summary>
32+
/// Retrieves information about a local upgrade package from a stream.
33+
/// </summary>
34+
/// <remarks>This method performs an asynchronous operation to extract and analyze the contents of the
35+
/// provided package archive. Ensure that the <paramref name="archiveStream"/> is not disposed or closed before the
36+
/// operation completes.</remarks>
37+
/// <param name="packageName">The name of the package to be upgraded. Cannot be null or empty.</param>
38+
/// <param name="archiveStream">A stream containing the package archive. Must be readable and positioned at the start of the archive.</param>
39+
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
40+
/// <returns>A task that represents the asynchronous operation. The task result contains a <see cref="LocalUpgradeInfo"/>
41+
/// object with details about the upgrade package.</returns>
42+
Task<LocalUpgradeInfo> GetLocalUpgradeInfo(string packageName, Stream archiveStream, CancellationToken cancellationToken);
43+
44+
/// <summary>
45+
/// Deletes the specified local upgrade package.
46+
/// </summary>
47+
/// <remarks>This method performs an asynchronous operation to delete a local upgrade package identified
48+
/// by its name. Ensure that the package name is valid and that the operation is not cancelled prematurely by the
49+
/// provided cancellation token.</remarks>
50+
/// <param name="packageName">The name of the package to be deleted. Cannot be null or empty.</param>
51+
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
52+
/// <returns>A task that represents the asynchronous delete operation.</returns>
53+
Task DeleteLocalUpgrade(string packageName, CancellationToken cancellationToken);
54+
1955
/// <summary>Begins the process of upgrading the site to the next applicable version.</summary>
2056
/// <param name="upgrades">The list of available upgrades (from <see cref="GetLocalUpgrades"/>).</param>
2157
/// <param name="cancellationToken">The cancellation token.</param>
2258
/// <returns>A task indicating completion.</returns>
2359
Task StartLocalUpgrade(IReadOnlyList<LocalUpgradeInfo> upgrades, CancellationToken cancellationToken);
60+
61+
/// <summary>Begins the process of upgrading the site to the specified version.</summary>
62+
/// <param name="upgrade">The upgrade version (from <see cref="GetLocalUpgrades"/>).</param>
63+
/// <param name="cancellationToken">The cancellation token.</param>
64+
/// <returns>A task indicating completion.</returns>
65+
Task StartLocalUpgrade(LocalUpgradeInfo upgrade, CancellationToken cancellationToken);
2466
}

DNN Platform/Library/Services/Installer/LocalUpgradeInfo.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,10 @@ public record LocalUpgradeInfo
2020

2121
/// <summary>Gets or sets the version of the upgrade package.</summary>
2222
public Version Version { get; set; }
23+
24+
/// <summary>Gets or sets the minimal DNN Version to upgrade from.</summary>
25+
public Version MinDnnVersion { get; set; }
26+
27+
/// <summary>Gets or sets a value indicating whether the upgrade can be installed.</summary>
28+
public bool CanInstall { get; set; }
2329
}

DNN Platform/Library/Services/Installer/LocalUpgradeService.cs

Lines changed: 132 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ namespace DotNetNuke.Services.Installer;
2626
/// <summary>The default <see cref="ILocalUpgradeService"/> implementation.</summary>
2727
public class LocalUpgradeService : ILocalUpgradeService
2828
{
29+
private static readonly string[] DefaultUpgradeExclude = new[]
30+
{
31+
"App_Data/Database.mdf",
32+
"Config/DotNetNuke.config",
33+
"Install/InstallWizard",
34+
"favicon.ico",
35+
"robots.txt",
36+
"web.config",
37+
};
38+
2939
private readonly IApplicationInfo application;
3040
private readonly IApplicationStatusInfo appStatus;
3141
private readonly IDirectory directory;
@@ -49,62 +59,150 @@ public async Task<IReadOnlyList<LocalUpgradeInfo>> GetLocalUpgrades(Cancellation
4959
IReadOnlyList<LocalUpgradeInfo> localUpgrades;
5060
if (this.directory.Exists(this.UpgradeDirectoryPath))
5161
{
62+
// clean up any old temp files
63+
var tempFiles = this.directory.GetFiles(this.UpgradeDirectoryPath, "*.resources", SearchOption.TopDirectoryOnly);
64+
foreach (var tempFile in tempFiles)
65+
{
66+
try
67+
{
68+
File.Delete(tempFile);
69+
}
70+
catch (Exception ex)
71+
{
72+
Exceptions.LogException(ex);
73+
}
74+
}
75+
5276
var upgradeFiles = this.directory.GetFiles(this.UpgradeDirectoryPath, "*.zip", SearchOption.TopDirectoryOnly);
53-
localUpgrades = await Task.WhenAll(upgradeFiles.Select(file => GetLocalUpgradeInfo(file, this.application, cancellationToken)));
77+
localUpgrades = await Task.WhenAll(upgradeFiles.Select(file => this.GetLocalUpgradeInfo(file, cancellationToken)));
5478
}
5579
else
5680
{
5781
localUpgrades = [];
5882
}
5983

6084
return localUpgrades;
85+
}
6186

62-
static async Task<LocalUpgradeInfo> GetLocalUpgradeInfo(string file, IApplicationInfo application, CancellationToken cancellationToken)
87+
/// <inheritdoc />
88+
public async Task<LocalUpgradeInfo> GetLocalUpgradeInfo(string file, CancellationToken cancellationToken)
89+
{
90+
try
6391
{
64-
try
92+
using var archiveStream = File.OpenRead(file);
93+
return await this.GetLocalUpgradeInfo(Path.GetFileNameWithoutExtension(file), archiveStream, cancellationToken);
94+
}
95+
catch (Exception exception)
96+
{
97+
Exceptions.LogException(exception);
98+
return new LocalUpgradeInfo
6599
{
66-
using var archiveStream = File.OpenRead(file);
67-
var archive = new ZipArchive(archiveStream, ZipArchiveMode.Read);
68-
var mainAssemblyEntry = archive.FileEntries()
69-
.Where(entry => string.Equals("DotNetNuke.dll", entry.Name, StringComparison.OrdinalIgnoreCase))
70-
.Where(entry => string.Equals("bin", Path.GetDirectoryName(entry.FullName), StringComparison.OrdinalIgnoreCase))
71-
.SingleOrDefault();
72-
73-
Version mainAssemblyVersion = null;
74-
if (mainAssemblyEntry is not null)
75-
{
76-
mainAssemblyVersion = await ReadZippedAssemblyVersion(mainAssemblyEntry, cancellationToken);
77-
}
100+
PackageName = Path.GetFileNameWithoutExtension(file),
101+
Version = null,
102+
IsValid = false,
103+
IsOutdated = false,
104+
};
105+
}
106+
}
78107

79-
return new LocalUpgradeInfo
80-
{
81-
PackageName = Path.GetFileNameWithoutExtension(file),
82-
Version = mainAssemblyVersion,
83-
IsValid = mainAssemblyVersion is not null,
84-
IsOutdated = mainAssemblyVersion is not null && mainAssemblyVersion <= application.Version,
85-
};
108+
/// <inheritdoc />
109+
public async Task<LocalUpgradeInfo> GetLocalUpgradeInfo(string packageName, Stream archiveStream, CancellationToken cancellationToken)
110+
{
111+
try
112+
{
113+
var archive = new ZipArchive(archiveStream, ZipArchiveMode.Read);
114+
var mainAssemblyEntry = archive.FileEntries()
115+
.Where(entry => string.Equals("DotNetNuke.dll", entry.Name, StringComparison.OrdinalIgnoreCase))
116+
.Where(entry => string.Equals("bin", Path.GetDirectoryName(entry.FullName), StringComparison.OrdinalIgnoreCase))
117+
.SingleOrDefault();
118+
119+
Version mainAssemblyVersion = null;
120+
if (mainAssemblyEntry is not null)
121+
{
122+
mainAssemblyVersion = await ReadZippedAssemblyVersion(mainAssemblyEntry, cancellationToken);
86123
}
87-
catch (Exception exception)
124+
125+
var upgradeInfo = archive.FileEntries()
126+
.Where(entry => string.Equals("App_Data/Upgrade/upgrade.json", entry.FullName, StringComparison.OrdinalIgnoreCase))
127+
.SingleOrDefault();
128+
var upgradeSettings = new LocalUpgradeSettings
88129
{
89-
Exceptions.LogException(exception);
90-
return new LocalUpgradeInfo
91-
{
92-
PackageName = Path.GetFileNameWithoutExtension(file),
93-
Version = null,
94-
IsValid = false,
95-
IsOutdated = false,
96-
};
130+
MinimumDnnVersion = "0.0.0",
131+
};
132+
if (upgradeInfo is not null)
133+
{
134+
var upgradeInfoFile = upgradeInfo.ReadTextFile();
135+
upgradeSettings = Json.Deserialize<LocalUpgradeSettings>(upgradeInfoFile);
97136
}
137+
138+
var res = new LocalUpgradeInfo
139+
{
140+
PackageName = packageName,
141+
Version = mainAssemblyVersion,
142+
IsValid = mainAssemblyVersion is not null,
143+
IsOutdated = mainAssemblyVersion is not null && mainAssemblyVersion <= this.application.Version,
144+
MinDnnVersion = Version.Parse(upgradeSettings.MinimumDnnVersion),
145+
};
146+
res.CanInstall = res.IsValid
147+
&& !res.IsOutdated
148+
&& (res.MinDnnVersion is null || res.MinDnnVersion <= this.application.Version);
149+
150+
return res;
151+
}
152+
catch (Exception exception)
153+
{
154+
Exceptions.LogException(exception);
155+
return new LocalUpgradeInfo
156+
{
157+
PackageName = packageName,
158+
Version = null,
159+
IsValid = false,
160+
IsOutdated = false,
161+
MinDnnVersion = null,
162+
CanInstall = false,
163+
};
164+
}
165+
}
166+
167+
/// <inheritdoc />
168+
public async Task DeleteLocalUpgrade(string packageName, CancellationToken cancellationToken)
169+
{
170+
var upgrades = await this.GetLocalUpgrades(cancellationToken);
171+
var upgrade = upgrades.FirstOrDefault(u => u.PackageName.Equals(packageName, StringComparison.InvariantCultureIgnoreCase));
172+
var packagePath = Path.Combine(this.UpgradeDirectoryPath, upgrade.PackageName + ".zip");
173+
174+
if (File.Exists(packagePath))
175+
{
176+
File.Delete(packagePath);
98177
}
99178
}
100179

101180
/// <inheritdoc />
102181
public async Task StartLocalUpgrade(IReadOnlyList<LocalUpgradeInfo> upgrades, CancellationToken cancellationToken)
103182
{
104183
var upgrade = upgrades.Where(u => u.IsValid && !u.IsOutdated).OrderBy(u => u.Version).First();
184+
await this.StartLocalUpgrade(upgrade, cancellationToken);
185+
}
186+
187+
/// <inheritdoc />
188+
public async Task StartLocalUpgrade(LocalUpgradeInfo upgrade, CancellationToken cancellationToken)
189+
{
105190
var upgradeZipPath = Path.Combine(this.UpgradeDirectoryPath, upgrade.PackageName + ".zip");
106191
using var fileStream = File.OpenRead(upgradeZipPath);
107192
using var archive = new ZipArchive(fileStream, ZipArchiveMode.Read);
193+
var upgradeInfo = archive.FileEntries()
194+
.Where(entry => string.Equals("App_Data/Upgrade/upgrade.json", entry.FullName, StringComparison.OrdinalIgnoreCase))
195+
.SingleOrDefault();
196+
var upgradeSettings = new LocalUpgradeSettings
197+
{
198+
UpgradeExclude = DefaultUpgradeExclude,
199+
};
200+
if (upgradeInfo is not null)
201+
{
202+
var upgradeInfoFile = upgradeInfo.ReadTextFile();
203+
upgradeSettings = Json.Deserialize<LocalUpgradeSettings>(upgradeInfoFile);
204+
}
205+
108206
var assemblyEntries = archive.FileEntries()
109207
.Where(entry => string.Equals(".dll", Path.GetExtension(entry.Name), StringComparison.OrdinalIgnoreCase))
110208
.Where(entry => string.Equals("bin", Path.GetDirectoryName(entry.FullName), StringComparison.OrdinalIgnoreCase))
@@ -115,7 +213,9 @@ public async Task StartLocalUpgrade(IReadOnlyList<LocalUpgradeInfo> upgrades, Ca
115213
installer.Commit();
116214

117215
await FileSystemUtils.UnzipResourcesAsync(
118-
archive.FileEntries().Where(entry => !assemblyEntries.Contains(entry)),
216+
archive.FileEntries()
217+
.Where(entry => !assemblyEntries.Contains(entry))
218+
.Where(entry => !upgradeSettings.UpgradeExclude.Any(filter => entry.FullName.StartsWith(filter, StringComparison.OrdinalIgnoreCase))),
119219
this.appStatus.ApplicationMapPath,
120220
cancellationToken);
121221
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information
4+
5+
namespace DotNetNuke.Services.Installer;
6+
7+
using Newtonsoft.Json;
8+
9+
/// <summary>
10+
/// Represents settings for a local upgrade, including minimum DNN version and excluded upgrade files.
11+
/// </summary>
12+
[JsonObject]
13+
public class LocalUpgradeSettings
14+
{
15+
/// <summary>
16+
/// Gets or sets the minimum DNN version required for the upgrade.
17+
/// </summary>
18+
[JsonProperty("minimumDnnVersion")]
19+
public string MinimumDnnVersion { get; set; }
20+
21+
/// <summary>
22+
/// Gets or sets the list of upgrade exclusions.
23+
/// </summary>
24+
[JsonProperty("upgradeExclude")]
25+
public string[] UpgradeExclude { get; set; }
26+
}

DNN Platform/Website/App_Data/Upgrade/PlaceHolder.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"minimumDnnVersion": "10.02.00",
3+
"upgradeExclude": [
4+
"/favicon.ico",
5+
"/Robots.txt",
6+
"/web.config",
7+
"/App_Data/Database.mdf",
8+
"/App_Data/Database_log.LDF",
9+
"/bin/Providers/DotNetNuke.Providers.AspNetClientCapabilityProvider.dll",
10+
"/Config/DotNetNuke.config",
11+
"/Install/InstallWizard.aspx"
12+
]
13+
}

DNN Platform/Website/DotNetNuke.Website.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -588,7 +588,6 @@
588588
</Compile>
589589
<Content Include="admin\Menus\ModuleActions\dnnQuickSettings.js" />
590590
<Content Include="admin\Menus\ModuleActions\ModuleActions.less" />
591-
<Content Include="App_Data\Upgrade\PlaceHolder.txt" />
592591
<Content Include="Install\InstallWizard.aspx" />
593592
<Content Include="Licenses\Castle Core %28Apache%29.txt.resources" />
594593
<Content Include="Licenses\LiteDB %28MIT%29.txt.resources" />
@@ -606,6 +605,7 @@
606605
<AdditionalFiles Include="..\..\stylecop.json">
607606
<Link>stylecop.json</Link>
608607
</AdditionalFiles>
608+
<Content Include="App_Data\Upgrade\upgrade.json" />
609609
<None Include="compilerconfig.json" />
610610
<Compile Include="DesktopModules\Admin\EditExtension\AuthenticationEditor.ascx.cs">
611611
<DependentUpon>AuthenticationEditor.ascx</DependentUpon>
@@ -1268,6 +1268,7 @@
12681268
<Content Include="Install\Config\09.13.04.config" />
12691269
<Content Include="Providers\DataProviders\SqlDataProvider\09.13.04.SqlDataProvider" />
12701270
<Content Include="Providers\DataProviders\SqlDataProvider\09.13.08.SqlDataProvider" />
1271+
<Content Include="Install\Config\10.02.00.config" />
12711272
<None Include="web.Debug.config">
12721273
<DependentUpon>web.config</DependentUpon>
12731274
</None>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<configuration>
2+
<nodes configfile="web.config">
3+
<node path="/configuration/appSettings" action="add">
4+
<!-- Can be set to true to allow the image handler to display text passed in the querystring. -->
5+
<add key="AllowDnnUpgradeUpload" value="false" />
6+
</node>
7+
</nodes>
8+
</configuration>

DNN Platform/Website/development.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
<add key="DisableMobileViewSiteCookieName" value="dnn_NoMobile" />
6868
<!-- Can be set to true to allow the image handler to display text passed in the querystring. -->
6969
<add key="AllowDnnImagePlaceholderText" value="false" />
70+
<add key="AllowDnnUpgradeUpload" value="true" />
7071
</appSettings>
7172

7273
<system.web.webPages.razor>

0 commit comments

Comments
 (0)