mirror of
https://github.com/Stone-Red-Code/AlwaysUpToDate.git
synced 2026-09-04 08:56:04 +02:00
Support ZIP extraction with rootPath attribute in update URLs
This commit is contained in:
@@ -206,7 +206,7 @@ namespace AlwaysUpToDate
|
|||||||
|
|
||||||
if (version > assemblyVersion && Interlocked.CompareExchange(ref updating, 0, 0) == 0)
|
if (version > assemblyVersion && Interlocked.CompareExchange(ref updating, 0, 0) == 0)
|
||||||
{
|
{
|
||||||
updateUrl = updateItem.DownloadUrl;
|
updateUrl = updateItem.DownloadUrl?.Value;
|
||||||
pendingUpdateItem = updateItem;
|
pendingUpdateItem = updateItem;
|
||||||
if (!updateItem.IsMandatory)
|
if (!updateItem.IsMandatory)
|
||||||
{
|
{
|
||||||
@@ -260,6 +260,46 @@ namespace AlwaysUpToDate
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string NormalizeZipRootPath(string zipRootPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(zipRootPath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string normalized = zipRootPath.Replace('\\', '/');
|
||||||
|
if (!normalized.EndsWith("/"))
|
||||||
|
{
|
||||||
|
normalized += "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetCommonRootFolder(ZipArchive archive)
|
||||||
|
{
|
||||||
|
ZipArchiveEntry firstFile = archive.Entries.FirstOrDefault(e => !string.IsNullOrEmpty(e.Name));
|
||||||
|
if (firstFile == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int slashIndex = firstFile.FullName.IndexOf('/');
|
||||||
|
if (slashIndex < 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string root = firstFile.FullName.Substring(0, slashIndex + 1);
|
||||||
|
|
||||||
|
if (archive.Entries.All(e => e.FullName.StartsWith(root, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task DownloadFile()
|
private async Task DownloadFile()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -309,42 +349,59 @@ namespace AlwaysUpToDate
|
|||||||
using (ZipArchive zipArchive = ZipFile.OpenRead(zipPath))
|
using (ZipArchive zipArchive = ZipFile.OpenRead(zipPath))
|
||||||
{
|
{
|
||||||
string fullInstallPath = Path.GetFullPath(installPath + Path.DirectorySeparatorChar);
|
string fullInstallPath = Path.GetFullPath(installPath + Path.DirectorySeparatorChar);
|
||||||
|
string commonRoot = NormalizeZipRootPath(pendingUpdateItem?.DownloadUrl?.RootPath) ?? GetCommonRootFolder(zipArchive);
|
||||||
long totalEntries = zipArchive.Entries.Count;
|
long totalEntries = zipArchive.Entries.Count;
|
||||||
long processedEntries = 0;
|
long processedEntries = 0;
|
||||||
|
|
||||||
foreach (ZipArchiveEntry entry in zipArchive.Entries)
|
foreach (ZipArchiveEntry entry in zipArchive.Entries)
|
||||||
{
|
{
|
||||||
string destinationPath = Path.GetFullPath(Path.Join(installPath, entry.FullName));
|
string entryRelativePath = commonRoot != null
|
||||||
|
? entry.FullName.Substring(commonRoot.Length)
|
||||||
|
: entry.FullName;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(entryRelativePath))
|
||||||
|
{
|
||||||
|
processedEntries++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string destinationPath = Path.GetFullPath(Path.Join(installPath, entryRelativePath));
|
||||||
if (!destinationPath.StartsWith(fullInstallPath, StringComparison.OrdinalIgnoreCase))
|
if (!destinationPath.StartsWith(fullInstallPath, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
processedEntries++;
|
processedEntries++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (File.Exists(Path.Join(installPath, entry.FullName)))
|
if (File.Exists(Path.Join(installPath, entryRelativePath)))
|
||||||
{
|
{
|
||||||
string moveName = Path.GetFileNameWithoutExtension(entry.FullName) + "_OLD_" + Path.GetExtension(entry.FullName);
|
string moveName = Path.GetFileNameWithoutExtension(entryRelativePath) + "_OLD_" + Path.GetExtension(entryRelativePath);
|
||||||
int counter = 1;
|
int counter = 1;
|
||||||
|
|
||||||
while (File.Exists(Path.Join(installPath, moveName)))
|
while (File.Exists(Path.Join(installPath, moveName)))
|
||||||
{
|
{
|
||||||
moveName = Path.GetFileNameWithoutExtension(entry.FullName) + "_OLD_" + counter + Path.GetExtension(entry.FullName);
|
moveName = Path.GetFileNameWithoutExtension(entryRelativePath) + "_OLD_" + counter + Path.GetExtension(entryRelativePath);
|
||||||
counter++;
|
counter++;
|
||||||
}
|
}
|
||||||
|
|
||||||
File.Move(Path.Join(installPath, entry.FullName), Path.Join(installPath, moveName));
|
File.Move(Path.Join(installPath, entryRelativePath), Path.Join(installPath, moveName));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.FullName.EndsWith('/'))
|
if (entryRelativePath.EndsWith('/'))
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(Path.Join(installPath, entry.FullName)))
|
if (!Directory.Exists(Path.Join(installPath, entryRelativePath)))
|
||||||
{
|
{
|
||||||
_ = Directory.CreateDirectory(Path.Join(installPath, entry.FullName));
|
_ = Directory.CreateDirectory(Path.Join(installPath, entryRelativePath));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
entry.ExtractToFile(Path.Join(installPath, entry.FullName), true);
|
string entryDirectory = Path.GetDirectoryName(Path.Join(installPath, entryRelativePath));
|
||||||
|
if (!string.IsNullOrEmpty(entryDirectory) && !Directory.Exists(entryDirectory))
|
||||||
|
{
|
||||||
|
_ = Directory.CreateDirectory(entryDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.ExtractToFile(Path.Join(installPath, entryRelativePath), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
processedEntries++;
|
processedEntries++;
|
||||||
|
|||||||
@@ -62,10 +62,10 @@ namespace AlwaysUpToDate
|
|||||||
public string Version { get; set; }
|
public string Version { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the URL from which the update ZIP file can be downloaded.
|
/// Gets or sets the download URL and optional ZIP root path for this update.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[XmlElement("url")]
|
[XmlElement("url")]
|
||||||
public string DownloadUrl { get; set; }
|
public DownloadUrl DownloadUrl { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets an optional URL pointing to a changelog for this update.
|
/// Gets or sets an optional URL pointing to a changelog for this update.
|
||||||
@@ -181,6 +181,29 @@ namespace AlwaysUpToDate
|
|||||||
public string Value { get; set; }
|
public string Value { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a download URL with an optional root path inside the ZIP file.
|
||||||
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// <c><url rootPath="release/net8.0">https://example.com/app.zip</url></c>
|
||||||
|
/// </example>
|
||||||
|
public class DownloadUrl
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an optional path prefix within the ZIP file to treat as the extraction root.
|
||||||
|
/// Only entries under this path are extracted, and the prefix is stripped from their destination paths.
|
||||||
|
/// When <see langword="null"/> (default), the updater auto-detects a single common root folder.
|
||||||
|
/// </summary>
|
||||||
|
[XmlAttribute("rootPath")]
|
||||||
|
public string RootPath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the URL from which the update ZIP file can be downloaded.
|
||||||
|
/// </summary>
|
||||||
|
[XmlText]
|
||||||
|
public string Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Specifies the target operating system for an update item.
|
/// Specifies the target operating system for an update item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ The manifest is an XML file with one `<item>` per target platform.
|
|||||||
| `os` | Yes | Target platform. Can be just an OS (`windows`, `linux`, `macos`/`osx`) or a combined OS-architecture value (`osx-arm64`, `windows-x64`, `linux-arm64`, etc.). |
|
| `os` | Yes | Target platform. Can be just an OS (`windows`, `linux`, `macos`/`osx`) or a combined OS-architecture value (`osx-arm64`, `windows-x64`, `linux-arm64`, etc.). |
|
||||||
| `arch` | No | Target architecture (`x86`, `x64`, `arm`, `arm64`, `any`). Overrides any architecture embedded in `<os>`. Only needed when using the separate-element format. |
|
| `arch` | No | Target architecture (`x86`, `x64`, `arm`, `arm64`, `any`). Overrides any architecture embedded in `<os>`. Only needed when using the separate-element format. |
|
||||||
| `version` | Yes | Version in `X.X.X.X` format. |
|
| `version` | Yes | Version in `X.X.X.X` format. |
|
||||||
| `url` | Yes | URL to the update ZIP file. |
|
| `url` | Yes | URL to the update ZIP file. Supports an optional `rootPath` attribute to specify a path prefix inside the ZIP to treat as the extraction root (e.g., `<url rootPath="release/net8.0">...</url>`). When omitted, the updater auto-detects a single common root folder. |
|
||||||
| `changelog` | No | URL pointing to a changelog. Passed to the `UpdateAvailable` event. |
|
| `changelog` | No | URL pointing to a changelog. Passed to the `UpdateAvailable` event. |
|
||||||
| `mandatory` | No | `true` or `false` (default `false`). Mandatory updates are installed immediately. |
|
| `mandatory` | No | `true` or `false` (default `false`). Mandatory updates are installed immediately. |
|
||||||
| `checksum` | No | Hex-encoded hash of the ZIP file. The `algorithm` attribute can be `SHA1`, `MD5`, `SHA256`, or `SHA512`. |
|
| `checksum` | No | Hex-encoded hash of the ZIP file. The `algorithm` attribute can be `SHA1`, `MD5`, `SHA256`, or `SHA512`. |
|
||||||
|
|||||||
Reference in New Issue
Block a user