From b1a7d3a11121284e7121e143d2e9276e216cdd10 Mon Sep 17 00:00:00 2001 From: Macocian Alexandru Victor Date: Wed, 22 Nov 2023 21:13:01 +0100 Subject: [PATCH] Improve update procedure (#496) * Improve update procedure * Change release procedure * Fix gh release * Fix release tag * Change release structure to support folders * Revert zip file changes to make them backwards compatible * Change path * Make launcher not contain the entire dependencies * Add downtime after API rate limit * Add rate limit checking before create release * Enumerate through rates * Adjust rate limit call * Change to create release as draft * Add update logic based on azure blobs * Fix publish * Create zip * Disable CD pipeline for prs * Increment version * Fix tests --- .github/workflows/cd.yaml | 20 ++- Daybreak.Installer/Daybreak.Installer.csproj | 6 +- Daybreak.Installer/Program.cs | 121 ++++++++++++++---- Daybreak.Tests/Daybreak.Tests.csproj | 1 + Daybreak.sln | 3 +- .../Configuration/Options/LauncherOptions.cs | 4 + Daybreak/Daybreak.csproj | 4 +- .../Services/Updater/ApplicationUpdater.cs | 118 ++++++++++++++++- Daybreak/Services/Updater/Models/Metadata.cs | 14 ++ README.md | 4 + Scripts/BuildRelease.ps1 | 35 +++-- Scripts/PushFilesToBlobStorage.ps1 | 20 +++ 12 files changed, 304 insertions(+), 46 deletions(-) create mode 100644 Daybreak/Services/Updater/Models/Metadata.cs create mode 100644 Scripts/PushFilesToBlobStorage.ps1 diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index aa8623c0..8cf5badd 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -83,7 +83,7 @@ jobs: echo "::set-env name=Version::$version" - name: Create publish launcher files - run: dotnet publish .\Daybreak\Daybreak.csproj -c $env:Configuration -r $env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=true -p:PublishTrimmed --self-contained true -o .\Publish + run: dotnet publish .\Daybreak\Daybreak.csproj -c $env:Configuration -r $env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=false --self-contained true -o .\Publish env: RuntimeIdentifier: win-${{ matrix.targetplatform }} @@ -93,7 +93,7 @@ jobs: RuntimeIdentifier: win-${{ matrix.targetplatform }} - name: Create publish extractor files - run: dotnet publish .\Daybreak.7ZipExtractor\Daybreak.7ZipExtractor.csproj -c $env:Configuration -r $env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=true --self-contained true -o .\Publish + run: dotnet publish .\Daybreak.7ZipExtractor\Daybreak.7ZipExtractor.csproj -c $env:Configuration -r $env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=false --self-contained true -o .\Publish env: RuntimeIdentifier: win-${{ matrix.targetplatform }} @@ -103,7 +103,13 @@ jobs: .\Scripts\BuildRelease.ps1 -version $env:Version shell: pwsh - - name: Publish release + - name: Publish blob files + run: | + Write-Host $env + .\Scripts\PushFilesToBlobStorage.ps1 -version $env:Version -sourcePath .\Publish\ -connectionString "${{ secrets.BLOBSTORAGE_CONNECTIONSTRING }}" + shell: pwsh + + - name: Create release draft uses: Xotl/cool-github-releases@v1.1.8 with: mode: update @@ -113,6 +119,14 @@ jobs: github_token: ${{ env.GITHUB_TOKEN }} replace_assets: true body_mrkdwn: ${{ env.Changelog }} + isDraft: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish release + run: | + gh release edit v${{ env.Version }} --draft=false + shell: powershell env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/Daybreak.Installer/Daybreak.Installer.csproj b/Daybreak.Installer/Daybreak.Installer.csproj index 59d69650..63c5d1b3 100644 --- a/Daybreak.Installer/Daybreak.Installer.csproj +++ b/Daybreak.Installer/Daybreak.Installer.csproj @@ -5,7 +5,11 @@ net8.0 enable enable - AnyCPU;x86;x64 + AnyCPU;x86 + True + True + True + win-x86 diff --git a/Daybreak.Installer/Program.cs b/Daybreak.Installer/Program.cs index 1254ce3a..48b1aa5b 100644 --- a/Daybreak.Installer/Program.cs +++ b/Daybreak.Installer/Program.cs @@ -1,33 +1,116 @@ // See https://aka.ms/new-console-template for more information using System.Diagnostics; using System.IO.Compression; +using System.Text; + +static void RenderProgressBar(int currentStep, int totalSteps, int barSize) +{ + Console.CursorLeft = 0; // Reset cursor position to the start of the line + Console.Write("["); // Start of the progress bar + + double pctComplete = (double)currentStep / totalSteps; + int chars = (int)Math.Round(pctComplete * barSize); + + Console.Write(new string('=', chars)); // The 'completed' part of the bar + Console.Write(new string(' ', barSize - chars)); // The 'remaining' part of the bar + + Console.Write("] "); + Console.Write($"{pctComplete:P0}"); // Display the percentage completed +} const string tempFile = "tempfile.zip"; +const string updatePkg = "update.pkg"; const string executableName = "Daybreak.exe"; Console.Title = "Daybreak Installer"; Console.WriteLine("Starting installation..."); -if (File.Exists(tempFile) is false) -{ - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine("Unable to find launcher package. Aborting installation"); - Console.ReadKey(); - return; -} - while (Process.GetProcesses().Where(p => p.ProcessName == "Daybreak").FirstOrDefault()?.HasExited is false) { Console.WriteLine($"Detected Daybreak process is still running. Waiting 5s and retrying"); await Task.Delay(5000); } -Console.WriteLine("Unpacking files..."); -try -{ - ZipFile.ExtractToDirectory(tempFile, AppContext.BaseDirectory, true); -} -catch +if (File.Exists(updatePkg)) { + Console.WriteLine("Unpacking files..."); + using var fileStream = new FileStream(updatePkg, FileMode.Open); + var sizeBuffer = new byte[4]; + var copyBuffer = new byte[1024]; + while (fileStream.Position < fileStream.Length - 1) + { + /* + * For each file downloaded, we write to the package the following: + * Size of the name string + * UTF-8 encoded name string + * Size of the relative path string + * UTF-8 encoded relative path string + * Size of the binary + * Binary data + */ + + await fileStream.ReadAsync(sizeBuffer, 0, 4); + var fileNameSize = BitConverter.ToInt32(sizeBuffer, 0); + var fileNameBuffer = new byte[fileNameSize]; + await fileStream.ReadAsync(fileNameBuffer, 0, fileNameSize); + var fileName = Encoding.UTF8.GetString(fileNameBuffer); + + RenderProgressBar((int)fileStream.Position, (int)fileStream.Length, 40); + + await fileStream.ReadAsync(sizeBuffer, 0, 4); + var relativePathSize = BitConverter.ToInt32(sizeBuffer, 0); + var relativePathBuffer = new byte[relativePathSize]; + await fileStream.ReadAsync(relativePathBuffer, 0, relativePathSize); + var relativePath = Encoding.UTF8.GetString(relativePathBuffer); + + RenderProgressBar((int)fileStream.Position, (int)fileStream.Length, 40); + + await fileStream.ReadAsync(sizeBuffer, 0, 4); + var binarySize = BitConverter.ToInt32(sizeBuffer, 0); + var fileInfo = new FileInfo(relativePath); + fileInfo.Directory!.Create(); + using var destinationStream = new FileStream(relativePath, FileMode.Create); + while(binarySize > 0) + { + var toRead = Math.Min(binarySize, copyBuffer.Length); + await fileStream.ReadAsync(copyBuffer, 0, toRead); + await destinationStream.WriteAsync(copyBuffer, 0, toRead); + binarySize -= toRead; + + RenderProgressBar((int)fileStream.Position, (int)fileStream.Length, 40); + } + } + + fileStream.Close(); + fileStream.Dispose(); + File.Delete(updatePkg); +} +else if (File.Exists(tempFile)) +{ + Console.WriteLine("Unpacking files..."); + try + { + ZipFile.ExtractToDirectory(tempFile, AppContext.BaseDirectory, true); + } + catch + { + } + + Console.WriteLine("Deleting package"); + try + { + File.Delete(tempFile); + } + catch (Exception e) + { + Console.WriteLine($"Failed to delete {tempFile}.\n{e}"); + } +} +else +{ + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("Unable to find launcher package. Aborting installation"); + Console.ReadKey(); + return; } Console.WriteLine("Deleting browser caches"); @@ -46,16 +129,6 @@ catch(Exception) { } -Console.WriteLine("Deleting package"); -try -{ - File.Delete(tempFile); -} -catch (Exception e) -{ - Console.WriteLine($"Failed to delete {tempFile}.\n{e}"); -} - Console.WriteLine("Launching application"); var process = new Process { diff --git a/Daybreak.Tests/Daybreak.Tests.csproj b/Daybreak.Tests/Daybreak.Tests.csproj index d19e66af..ff8a75e9 100644 --- a/Daybreak.Tests/Daybreak.Tests.csproj +++ b/Daybreak.Tests/Daybreak.Tests.csproj @@ -4,6 +4,7 @@ net8.0-windows false AnyCPU;x86;x64 + win-x86 diff --git a/Daybreak.sln b/Daybreak.sln index b4923aaf..b07aaebc 100644 --- a/Daybreak.sln +++ b/Daybreak.sln @@ -23,6 +23,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{41AE Scripts\BuildRelease.ps1 = Scripts\BuildRelease.ps1 Scripts\CompareVersions.ps1 = Scripts\CompareVersions.ps1 Scripts\GetBuildVersion.ps1 = Scripts\GetBuildVersion.ps1 + Scripts\PushFilesToBlobStorage.ps1 = Scripts\PushFilesToBlobStorage.ps1 EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0E30DDA0-2111-4E6C-851E-01DC34124FC2}" @@ -31,7 +32,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .github\CODEOWNERS = .github\CODEOWNERS EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Daybreak.7ZipExtractor", "Daybreak.7ZipExtractor\Daybreak.7ZipExtractor.csproj", "{CA60F9AC-A496-4DB6-970E-ECE73B051C05}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Daybreak.7ZipExtractor", "Daybreak.7ZipExtractor\Daybreak.7ZipExtractor.csproj", "{CA60F9AC-A496-4DB6-970E-ECE73B051C05}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Daybreak/Configuration/Options/LauncherOptions.cs b/Daybreak/Configuration/Options/LauncherOptions.cs index 1dc74617..126680a8 100644 --- a/Daybreak/Configuration/Options/LauncherOptions.cs +++ b/Daybreak/Configuration/Options/LauncherOptions.cs @@ -49,4 +49,8 @@ public sealed class LauncherOptions [JsonProperty(nameof(PersistentLogging))] [OptionName(Name = "Persistent Logging", Description = "If true, the launcher will save logs in the local database. Otherwise, the launcher will only keep logs in a memory cache")] public bool PersistentLogging { get; set; } = false; + + [JsonProperty(nameof(BetaUpdate))] + [OptionName(Name = "Beta Update", Description = "If true, the launcher will use the new update procedure")] + public bool BetaUpdate { get; set; } = false; } diff --git a/Daybreak/Daybreak.csproj b/Daybreak/Daybreak.csproj index ca19e4dd..d696bb99 100644 --- a/Daybreak/Daybreak.csproj +++ b/Daybreak/Daybreak.csproj @@ -7,16 +7,16 @@ net8.0-windows true enable - true AnyCPU;x86;x64 true preview Daybreak.ico true - 0.9.8.152 + 0.9.8.153 true cfb2a489-db80-448d-a969-80270f314c46 True + win-x86 diff --git a/Daybreak/Services/Updater/ApplicationUpdater.cs b/Daybreak/Services/Updater/ApplicationUpdater.cs index db5c4b4c..1d24456e 100644 --- a/Daybreak/Services/Updater/ApplicationUpdater.cs +++ b/Daybreak/Services/Updater/ApplicationUpdater.cs @@ -4,8 +4,11 @@ using Daybreak.Models.Github; using Daybreak.Models.Progress; using Daybreak.Services.Downloads; using Daybreak.Services.Notifications; +using Daybreak.Services.Privilege; using Daybreak.Services.Registry; +using Daybreak.Services.Updater.Models; using Daybreak.Services.Updater.PostUpdate; +using Daybreak.Views; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -13,11 +16,16 @@ using System.Configuration; using System.Core.Extensions; using System.Diagnostics; using System.Extensions; +using System.IO; using System.Linq; using System.Net.Http; +using System.Net.Http.Json; using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; using System.Threading; using System.Threading.Tasks; +using static Daybreak.Utils.NativeMethods; using Version = Daybreak.Models.Versioning.Version; namespace Daybreak.Services.Updater; @@ -28,12 +36,15 @@ internal sealed class ApplicationUpdater : IApplicationUpdater private const string UpdatedKey = "LauncherUpdating"; private const string TempFile = "tempfile.zip"; private const string VersionTag = "{VERSION}"; + private const string FileTag = "{FILE}"; private const string RefTagPrefix = "/refs/tags"; private const string VersionListUrl = "https://api.github.com/repos/AlexMacocian/Daybreak/git/refs/tags"; private const string Url = "https://github.com/AlexMacocian/Daybreak/releases/latest"; private const string DownloadUrl = $"https://github.com/AlexMacocian/Daybreak/releases/download/{VersionTag}/Daybreak{VersionTag}.zip"; + private const string BlobStorageUrl = $"https://daybreak.blob.core.windows.net/{VersionTag}/{FileTag}"; private readonly CancellationTokenSource updateCancellationTokenSource = new(); + private readonly IPrivilegeManager privilegeManager; private readonly INotificationService notificationService; private readonly IRegistryService registryService; private readonly IDownloadService downloadService; @@ -45,6 +56,7 @@ internal sealed class ApplicationUpdater : IApplicationUpdater public Version CurrentVersion { get; } public ApplicationUpdater( + IPrivilegeManager privilegeManager, INotificationService notificationService, IRegistryService registryService, IDownloadService downloadService, @@ -53,6 +65,7 @@ internal sealed class ApplicationUpdater : IApplicationUpdater IHttpClient httpClient, ILogger logger) { + this.privilegeManager = privilegeManager.ThrowIfNull(); this.notificationService = notificationService.ThrowIfNull(); this.registryService = registryService.ThrowIfNull(); this.downloadService = downloadService.ThrowIfNull(); @@ -78,17 +91,36 @@ internal sealed class ApplicationUpdater : IApplicationUpdater public async Task DownloadUpdate(Version version, UpdateStatus updateStatus) { - updateStatus.CurrentStep = DownloadStatus.InitializingDownload; - var uri = DownloadUrl.Replace(VersionTag, version.ToString()); - if (await this.downloadService.DownloadFile(uri, TempFile, updateStatus) is false) + if (!this.privilegeManager.AdminPrivileges) { - this.logger.LogError("Failed to download update file"); + this.privilegeManager.RequestAdminPrivileges("Daybreak needs Administrator privileges in order to update"); return false; } - updateStatus.CurrentStep = UpdateStatus.PendingRestart; - this.logger.LogInformation("Downloaded update file"); - return true; + if (version.HasPrefix is false) + { + version.HasPrefix = true; + } + + if (!this.liveOptions.Value.BetaUpdate) + { + return await this.DownloadUpdateInternalLegacy(version, updateStatus); + } + + var maybeMetadataResponse = await this.httpClient.GetAsync( + BlobStorageUrl + .Replace(VersionTag, version.ToString().Replace(".", "-")) + .Replace(FileTag, "Metadata.json")); + if (maybeMetadataResponse.IsSuccessStatusCode) + { + var metaData = await maybeMetadataResponse.Content.ReadFromJsonAsync>(); + if (metaData is not null) + { + return await this.DownloadUpdateInternalBlob(metaData, version, updateStatus); + } + } + + return await this.DownloadUpdateInternalLegacy(version, updateStatus); } public async Task DownloadLatestUpdate(UpdateStatus updateStatus) @@ -177,6 +209,78 @@ internal sealed class ApplicationUpdater : IApplicationUpdater { } + private async Task DownloadUpdateInternalBlob(List metadata, Version version, UpdateStatus updateStatus) + { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.DownloadUpdateInternalBlob), version.ToString()); + // Exclude daybreak packed files + var daybreakArchive = $"daybreak{version}.zip"; + var filesToDownload = metadata + .Where(m => m.Name != daybreakArchive) + .Where(m => + { + var fileInfo = new FileInfo(m.RelativePath!); + return !fileInfo.Exists || fileInfo.Length != m.Size; + }) + .ToList(); + + updateStatus.CurrentStep = DownloadStatus.InitializingDownload; + using var packageStream = new FileStream("update.pkg", FileMode.Create); + var downloaded = 0d; + var downloadBuffer = new byte[1024]; + var sizeToDownload = (double)filesToDownload.Sum(m => m.Size); + var sw = Stopwatch.StartNew(); + foreach (var file in filesToDownload) + { + var downloadUrl = BlobStorageUrl.Replace(VersionTag, version.ToString().Replace('.', '-')).Replace(FileTag, file.RelativePath); + var response = await this.httpClient.GetAsync(downloadUrl); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Error {response.StatusCode} when downloading {file.RelativePath}"); + return false; + } + + var fileNameBytes = Encoding.UTF8.GetBytes(file.Name!); + var relativePathBytes = Encoding.UTF8.GetBytes(file.RelativePath!); + await packageStream.WriteAsync(BitConverter.GetBytes(fileNameBytes.Length)); + await packageStream.WriteAsync(fileNameBytes); + await packageStream.WriteAsync(BitConverter.GetBytes(relativePathBytes.Length)); + await packageStream.WriteAsync(relativePathBytes); + await packageStream.WriteAsync(BitConverter.GetBytes(file.Size)); + var downloadStream = await response.Content.ReadAsStreamAsync(); + var fileSize = file.Size; + while (fileSize > 0) + { + var toGet = Math.Min(fileSize, 1024); + await downloadStream.ReadAsync(downloadBuffer, 0, toGet); + fileSize -= toGet; + await packageStream.WriteAsync(downloadBuffer, 0, toGet); + + downloaded += toGet; + var etaMillis = sw.ElapsedMilliseconds * (sizeToDownload - downloaded) / downloaded; + updateStatus.CurrentStep = DownloadStatus.Downloading(downloaded / sizeToDownload, TimeSpan.FromMilliseconds(etaMillis)); + } + } + + scopedLogger.LogInformation($"Prepared update package at {Path.GetFullPath("update.pkg")}"); + updateStatus.CurrentStep = UpdateStatus.PendingRestart; + return true; + } + + private async Task DownloadUpdateInternalLegacy(Version version, UpdateStatus updateStatus) + { + updateStatus.CurrentStep = DownloadStatus.InitializingDownload; + var uri = DownloadUrl.Replace(VersionTag, version.ToString()); + if (await this.downloadService.DownloadFile(uri, TempFile, updateStatus) is false) + { + this.logger.LogError("Failed to download update file"); + return false; + } + + updateStatus.CurrentStep = UpdateStatus.PendingRestart; + this.logger.LogInformation("Downloaded update file"); + return true; + } + private async Task GetLatestVersion() { using var response = await this.httpClient.GetAsync(Url); diff --git a/Daybreak/Services/Updater/Models/Metadata.cs b/Daybreak/Services/Updater/Models/Metadata.cs new file mode 100644 index 00000000..765b3b3d --- /dev/null +++ b/Daybreak/Services/Updater/Models/Metadata.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Daybreak.Services.Updater.Models; +internal sealed class Metadata +{ + [JsonProperty(nameof(RelativePath))] + public string? RelativePath { get; set; } + + [JsonProperty(nameof(Name))] + public string? Name { get; set; } + + [JsonProperty(nameof(Size))] + public int Size { get; set; } +} diff --git a/README.md b/README.md index 911fbaf2..0d654cc6 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,8 @@ Custom launcher for Guildwars. Requires webview2 runtime https://go.microsoft.com/fwlink/p/?LinkId=2124703. +Download: +- Go to [Releases](https://github.com/AlexMacocian/Daybreak/releases/latest) +- Download daybreakv[VERSION].zip, where [VERSION] is the version of the release + Please check the [wiki](https://github.com/AlexMacocian/Daybreak/wiki) for project description and features diff --git a/Scripts/BuildRelease.ps1 b/Scripts/BuildRelease.ps1 index ccaa5103..bf135296 100644 --- a/Scripts/BuildRelease.ps1 +++ b/Scripts/BuildRelease.ps1 @@ -3,17 +3,36 @@ Param( [string]$version ) #end param +function Get-FileMetadata { + param ( + [string]$Path + ) + + $fileInfo = Get-Item $Path + + # Temporarily change current location to the folder path + $currentLocation = Get-Location + Set-Location -Path .\Publish + $relativePath = Resolve-Path -Path $Path -Relative + Set-Location -Path $currentLocation + return @{ + Name = $fileInfo.Name + Size = $fileInfo.Length + RelativePath = $relativePath.trim(".\\") + } +} + +$zipPath = "Publish\daybreakv$version.zip" Write-Output "Deleting pdb file" Remove-item .\Publish\Daybreak.pdb Remove-item .\Publish\Daybreak.Installer.pdb Remove-item .\Publish\Daybreak.7ZipExtractor.pdb Move-Item -Path .\Publish\Daybreak.Installer.exe -Destination .\Publish\Daybreak.Installer.Temp.exe -$zipPath = "Publish\daybreakv$version.zip" -Write-Output "Compressing binaries to $zipPath" + Compress-Archive .\Publish\* $zipPath -Force -Write-Output "Cleaning up files" -foreach ($file in Get-ChildItem -Path .\Publish -Exclude *.zip -Recurse -ErrorAction SilentlyContinue) -{ - Write-Output "Removing $file" - Remove-item $file -Recurse -Force -ErrorAction SilentlyContinue -} \ No newline at end of file + +$files = Get-ChildItem -Path .\Publish -Recurse -File +$metadata = $files | ForEach-Object { Get-FileMetadata $_.FullName } +$json = $metadata | ConvertTo-Json +$json | Out-File -FilePath ".\Publish\Metadata.json" +Write-Host "Metadata written to .\Publish\Metadata.json" \ No newline at end of file diff --git a/Scripts/PushFilesToBlobStorage.ps1 b/Scripts/PushFilesToBlobStorage.ps1 new file mode 100644 index 00000000..0254189d --- /dev/null +++ b/Scripts/PushFilesToBlobStorage.ps1 @@ -0,0 +1,20 @@ +Param( + [Parameter(Mandatory=$true)] + [string]$version, + [Parameter(Mandatory=$true)] + [string]$connectionString, + [Parameter(Mandatory=$true)] + [string]$sourcePath +) + +# Create a new blob container +$containerName = "v$($version.ToLower().Replace('.', '-'))" # Container names must be lowercase +Write-Host "Creating container $($containerName)" +az storage container create --name $containerName --connection-string $connectionString + +Write-Host "Uploading files" +# Upload all files from the source path to the blob container +az storage blob upload-batch --destination $containerName --source $sourcePath --connection-string $connectionString + +Write-Host "Setting public access to container" +az storage container set-permission --name $containerName --public-access blob --connection-string $connectionString \ No newline at end of file