diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..a1cb220 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,32 @@ +name: Publish + +on: + workflow_dispatch: + # schedule: + # - cron: '0 10 * * *' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Install dependencies + run: sudo apt-get install -y python3-paramiko python3-lxml + - uses: actions/checkout@v3.6.0 + with: + submodules: 'recursive' + - name: Setup .NET Core + uses: actions/setup-dotnet@v3.2.0 + with: + dotnet-version: 9.0.x + - name: Install dependencies + run: dotnet restore + - name: Package launcher files + run: dotnet run Nebula.Packager + - name: FTP Deploy Release + uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + with: + server: ${{ secrets.FTP_SERVER }} + username: ${{ secrets.FTP_USERNAME }} + password: ${{ secrets.FTP_PASSWORD }} + local-dir: ./release/ + server-dir: ./release/ diff --git a/.gitignore b/.gitignore index add57be..e31ae9f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ bin/ obj/ /packages/ riderModule.iml -/_ReSharper.Caches/ \ No newline at end of file +/_ReSharper.Caches/ +release/ \ No newline at end of file diff --git a/.idea/.idea.Nebula/.idea/avalonia.xml b/.idea/.idea.Nebula/.idea/avalonia.xml index c1ea37a..47fa533 100644 --- a/.idea/.idea.Nebula/.idea/avalonia.xml +++ b/.idea/.idea.Nebula/.idea/avalonia.xml @@ -33,6 +33,8 @@ + + diff --git a/Nebula.Launcher/Nebula.Launcher.csproj b/Nebula.Launcher/Nebula.Launcher.csproj index 4ead1a4..7aa1311 100644 --- a/Nebula.Launcher/Nebula.Launcher.csproj +++ b/Nebula.Launcher/Nebula.Launcher.csproj @@ -4,6 +4,7 @@ net9.0 enable true + false app.manifest true true diff --git a/Nebula.Packager/Nebula.Packager.csproj b/Nebula.Packager/Nebula.Packager.csproj new file mode 100644 index 0000000..85b4959 --- /dev/null +++ b/Nebula.Packager/Nebula.Packager.csproj @@ -0,0 +1,10 @@ + + + + Exe + net9.0 + enable + enable + + + diff --git a/Nebula.Packager/Program.cs b/Nebula.Packager/Program.cs new file mode 100644 index 0000000..772f32d --- /dev/null +++ b/Nebula.Packager/Program.cs @@ -0,0 +1,74 @@ +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Nebula.Packager; +public static class Program +{ + public static void Main(string[] args) + { + Pack("","Release"); + } + + private static void Pack(string rootPath,string configuration) + { + var processInfo = new ProcessStartInfo + { + FileName = "dotnet", + ArgumentList = + { + "publish", + Path.Combine(rootPath,"Nebula.Launcher", "Nebula.Launcher.csproj"), + "-c", configuration, + } + }; + + var process = Process.Start(processInfo)!; + process.WaitForExit(); + if(process.ExitCode != 0) + throw new Exception($"Packager has exited with code {process.ExitCode}"); + + var destinationDirectory = Path.Combine(rootPath,"release"); + var sourceDirectory = Path.Combine("Nebula.Launcher", "bin", configuration,"publish"); + + if (Directory.Exists(destinationDirectory)) + { + Directory.Delete(destinationDirectory, true); + } + + Directory.CreateDirectory(destinationDirectory); + + HashSet entries = new HashSet(); + + foreach (var fileName in Directory.EnumerateFiles(sourceDirectory, "*.*", SearchOption.AllDirectories)) + { + using var md5 = MD5.Create(); + using var stream = File.OpenRead(fileName); + + var hash = md5.ComputeHash(stream); + var hashStr = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + + if(!File.Exists(Path.Combine(destinationDirectory, hashStr))) + File.Copy(fileName, Path.Combine(destinationDirectory, hashStr)); + + var fileNameCut = fileName.Remove(0, sourceDirectory.Length + 1); + + entries.Add(new LauncherManifestEntry(hashStr, fileNameCut)); + Console.WriteLine($"Added {hashStr} file name {fileNameCut}"); + } + + using var manifest = File.CreateText(Path.Combine(destinationDirectory, "manifest.json")); + manifest.AutoFlush = true; + manifest.Write(JsonSerializer.Serialize(new LauncherManifest(entries))); + } +} + +public record struct LauncherManifest( + [property: JsonPropertyName("entries")] HashSet Entries +); + +public record struct LauncherManifestEntry( + [property: JsonPropertyName("hash")] string Hash, + [property: JsonPropertyName("path")] string Path +); \ No newline at end of file diff --git a/Nebula.UpdateResolver/App.axaml b/Nebula.UpdateResolver/App.axaml new file mode 100644 index 0000000..e7cb2f2 --- /dev/null +++ b/Nebula.UpdateResolver/App.axaml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/Nebula.UpdateResolver/App.axaml.cs b/Nebula.UpdateResolver/App.axaml.cs new file mode 100644 index 0000000..47c2841 --- /dev/null +++ b/Nebula.UpdateResolver/App.axaml.cs @@ -0,0 +1,31 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Microsoft.Extensions.DependencyInjection; +using Nebula.Shared; + +namespace Nebula.UpdateResolver; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + var services = new ServiceCollection(); + services.AddServices(); + services.AddTransient(); + + var serviceProvider = services.BuildServiceProvider(); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = serviceProvider.GetService(); + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/Nebula.UpdateResolver/Assets/back.gif b/Nebula.UpdateResolver/Assets/back.gif new file mode 100644 index 0000000..c61d279 Binary files /dev/null and b/Nebula.UpdateResolver/Assets/back.gif differ diff --git a/Nebula.UpdateResolver/LauncherManifest.cs b/Nebula.UpdateResolver/LauncherManifest.cs new file mode 100644 index 0000000..ad9711e --- /dev/null +++ b/Nebula.UpdateResolver/LauncherManifest.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Nebula.UpdateResolver; + +public record struct LauncherManifest( + [property: JsonPropertyName("entries")] HashSet Entries +); + +public record struct LauncherManifestEntry( + [property: JsonPropertyName("hash")] string Hash, + [property: JsonPropertyName("path")] string Path + ); \ No newline at end of file diff --git a/Nebula.UpdateResolver/MainWindow.axaml b/Nebula.UpdateResolver/MainWindow.axaml new file mode 100644 index 0000000..25e3338 --- /dev/null +++ b/Nebula.UpdateResolver/MainWindow.axaml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Nebula.UpdateResolver/MainWindow.axaml.cs b/Nebula.UpdateResolver/MainWindow.axaml.cs new file mode 100644 index 0000000..2f8504a --- /dev/null +++ b/Nebula.UpdateResolver/MainWindow.axaml.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Controls; +using Nebula.Shared.FileApis.Interfaces; +using Nebula.Shared.Models; +using Nebula.Shared.Services; + +namespace Nebula.UpdateResolver; + +public partial class MainWindow : Window +{ + private readonly ConfigurationService _configurationService; + private readonly RestService _restService; + private readonly HttpClient _httpClient = new HttpClient(); + public IReadWriteFileApi FileApi { get; set; } + + public MainWindow(FileService fileService, ConfigurationService configurationService, RestService restService) + { + _configurationService = configurationService; + _restService = restService; + InitializeComponent(); + FileApi = fileService.CreateFileApi("app"); + + } + + private async Task DownloadFiles() + { + var info = await EnsureFiles(); + + foreach (var file in info.ToDelete) + { + FileApi.Remove(file.Path); + } + + foreach (var file in info.ToDownload) + { + using var response = _httpClient.GetAsync( + _configurationService.GetConfigValue(UpdateConVars.UpdateCacheUrl) + + "/" + file.Hash + ); + response.Result.EnsureSuccessStatusCode(); + await using var stream = await response.Result.Content.ReadAsStreamAsync(); + FileApi.Save(file.Path, stream); + } + } + + private async Task EnsureFiles() + { + var manifest = await _restService.GetAsync( + _configurationService.GetConfigValue(UpdateConVars.UpdateCacheUrl)!, CancellationToken.None); + + var toDownload = new HashSet(); + var toDelete = new HashSet(); + + if (_configurationService.TryGetConfigValue(UpdateConVars.CurrentLauncherManifest, out var currentManifest)) + { + foreach (var file in currentManifest.Entries) + { + if(!manifest.Entries.Contains(file)) + toDelete.Add(file); + } + + foreach (var file in manifest.Entries) + { + if(!currentManifest.Entries.Contains(file)) + toDownload.Add(file); + } + } + else + { + _configurationService.SetConfigValue(UpdateConVars.CurrentLauncherManifest, manifest); + toDownload = manifest.Entries; + } + + return new ManifestEnsureInfo(toDownload, toDelete); + } +} + +public record struct ManifestEnsureInfo(HashSet ToDownload, HashSet ToDelete); \ No newline at end of file diff --git a/Nebula.UpdateResolver/Nebula.UpdateResolver.csproj b/Nebula.UpdateResolver/Nebula.UpdateResolver.csproj new file mode 100644 index 0000000..8c95be5 --- /dev/null +++ b/Nebula.UpdateResolver/Nebula.UpdateResolver.csproj @@ -0,0 +1,31 @@ + + + WinExe + net9.0 + enable + true + app.manifest + true + + + + + + + + + + + + + + None + All + + + + + + + + diff --git a/Nebula.UpdateResolver/Program.cs b/Nebula.UpdateResolver/Program.cs new file mode 100644 index 0000000..4e0205d --- /dev/null +++ b/Nebula.UpdateResolver/Program.cs @@ -0,0 +1,21 @@ +using Avalonia; +using System; + +namespace Nebula.UpdateResolver; + +class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); +} \ No newline at end of file diff --git a/Nebula.UpdateResolver/UpdateCVars.cs b/Nebula.UpdateResolver/UpdateCVars.cs new file mode 100644 index 0000000..0709462 --- /dev/null +++ b/Nebula.UpdateResolver/UpdateCVars.cs @@ -0,0 +1,13 @@ +using System; +using Nebula.Shared.Models; +using Nebula.Shared.Services; + +namespace Nebula.UpdateResolver; + +public static class UpdateConVars +{ + public static readonly ConVar UpdateCacheUrl = + ConVarBuilder.Build("update.url",new Uri("https://cinka.ru/nebula-launcher/files/publish/release")); + public static readonly ConVar CurrentLauncherManifest = + ConVarBuilder.Build("update.manifest"); +} \ No newline at end of file diff --git a/Nebula.UpdateResolver/app.manifest b/Nebula.UpdateResolver/app.manifest new file mode 100644 index 0000000..0ef1ba1 --- /dev/null +++ b/Nebula.UpdateResolver/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/Nebula.sln b/Nebula.sln index d26176c..1f9ef03 100644 --- a/Nebula.sln +++ b/Nebula.sln @@ -10,6 +10,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nebula.Runner", "Nebula.Run EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nebula.SourceGenerators", "Nebula.SourceGenerators\Nebula.SourceGenerators.csproj", "{985A8F36-AFEB-4E85-8EDB-7C9DDEC698DC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nebula.UpdateResolver", "Nebula.UpdateResolver\Nebula.UpdateResolver.csproj", "{90CB754D-00A1-493D-A630-02BDA0AFF31A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nebula.Packager", "Nebula.Packager\Nebula.Packager.csproj", "{D444A5F9-4549-467F-9398-97DD6DB1E263}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,5 +40,13 @@ Global {985A8F36-AFEB-4E85-8EDB-7C9DDEC698DC}.Debug|Any CPU.Build.0 = Debug|Any CPU {985A8F36-AFEB-4E85-8EDB-7C9DDEC698DC}.Release|Any CPU.ActiveCfg = Release|Any CPU {985A8F36-AFEB-4E85-8EDB-7C9DDEC698DC}.Release|Any CPU.Build.0 = Release|Any CPU + {90CB754D-00A1-493D-A630-02BDA0AFF31A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90CB754D-00A1-493D-A630-02BDA0AFF31A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90CB754D-00A1-493D-A630-02BDA0AFF31A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90CB754D-00A1-493D-A630-02BDA0AFF31A}.Release|Any CPU.Build.0 = Release|Any CPU + {D444A5F9-4549-467F-9398-97DD6DB1E263}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D444A5F9-4549-467F-9398-97DD6DB1E263}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D444A5F9-4549-467F-9398-97DD6DB1E263}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D444A5F9-4549-467F-9398-97DD6DB1E263}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Nebula.sln.DotSettings.user b/Nebula.sln.DotSettings.user index cd39add..4fc2048 100644 --- a/Nebula.sln.DotSettings.user +++ b/Nebula.sln.DotSettings.user @@ -1,2 +1,3 @@  - ForceIncluded \ No newline at end of file + ForceIncluded + ForceIncluded \ No newline at end of file