Skip to content

Commit

Permalink
Add Flutter installer support
Browse files Browse the repository at this point in the history
  • Loading branch information
fahminlb33 committed Feb 28, 2021
1 parent df03486 commit aff4bee
Show file tree
Hide file tree
Showing 10 changed files with 856 additions and 6 deletions.
72 changes: 72 additions & 0 deletions src/KFlearning.Core/API/FlutterGitClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Threading.Tasks;

namespace KFlearning.Core.API
{
public interface IFlutterGitClient
{
string GetFlutterDownloadUri(string version);
Task<string> GetLatestFlutterVersion();
}

public class FlutterGitClient : IFlutterGitClient
{
private static HttpClient Client;

static FlutterGitClient()
{
var version = Assembly.GetCallingAssembly().GetName().Version;

Client = new HttpClient();
Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json"));
Client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("KFlearning", $"{version.Major}.{version.Minor}.{version.Build}"));
}

public async Task<string> GetLatestFlutterVersion()
{
var response = await Client.GetAsync("https://api.github.com/repos/flutter/flutter/git/refs/tags");
var serializer = new JsonSerializer();
using (var bodyReader = new StreamReader(await response.Content.ReadAsStreamAsync()))
using (var jsonReader = new JsonTextReader(bodyReader))
{
var tags = serializer.Deserialize<List<FlutterGitTag>>(jsonReader);
var latest = tags.Where(x => !x.Ref.Contains("pre")).LastOrDefault();
if (latest == null)
{
throw new KFlearningException("Tidak dapat menemukan versi Flutter! Silakan download manual.");
}

return GetVersionFromTag(latest.Ref);
}
}

public string GetFlutterDownloadUri(string version)
{
return $"https://storage.googleapis.com/flutter_infra/releases/stable/windows/flutter_windows_{version}-stable.zip";
}

private string GetVersionFromTag(string tag)
{
return tag.Split('/').Last();
}

public partial class FlutterGitTag
{
[JsonProperty("ref")]
public string Ref { get; set; }

[JsonProperty("node_id")]
public string NodeId { get; set; }

[JsonProperty("url")]
public Uri Url { get; set; }
}
}
}
19 changes: 17 additions & 2 deletions src/KFlearning/KFlearning.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,13 @@
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Services\FlutterInstallService.cs" />
<Compile Include="Services\KFlearningApplicationContext.cs" />
<Compile Include="Services\TelemetryService.cs" />
<Compile Include="TemplateProvider\TR.Designer.cs">
Expand Down Expand Up @@ -101,6 +104,12 @@
<Compile Include="Views\CreateProjectForm.Designer.cs">
<DependentUpon>CreateProjectForm.cs</DependentUpon>
</Compile>
<Compile Include="Views\FlutterInstallView.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Views\FlutterInstallView.Designer.cs">
<DependentUpon>FlutterInstallView.cs</DependentUpon>
</Compile>
<Compile Include="Views\StartupForm.cs">
<SubType>Form</SubType>
</Compile>
Expand Down Expand Up @@ -130,6 +139,9 @@
<EmbeddedResource Include="Views\CreateProjectForm.resx">
<DependentUpon>CreateProjectForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Views\FlutterInstallView.resx">
<DependentUpon>FlutterInstallView.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Views\StartupForm.resx">
<DependentUpon>StartupForm.cs</DependentUpon>
</EmbeddedResource>
Expand All @@ -150,10 +162,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Castle.Core">
<Version>4.4.0</Version>
<Version>4.4.1</Version>
</PackageReference>
<PackageReference Include="Castle.Windsor">
<Version>5.0.1</Version>
<Version>5.1.1</Version>
</PackageReference>
<PackageReference Include="DotNetZip">
<Version>1.15.0</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
Expand Down
247 changes: 247 additions & 0 deletions src/KFlearning/Services/FlutterInstallService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Ionic.Zip;
using KFlearning.Core.API;
using KFlearning.Core.Extensions;
using KFlearning.Core.Services;

namespace KFlearning.Services
{
public interface IFlutterInstallService
{
string FlutterVersion { get; }
string InstallPath { get; set; }

event EventHandler<FlutterInstallReadyEventArgs> InstallReady;
event EventHandler<FlutterInstallProgressEventArgs> ProgressChanged;
event EventHandler<FlutterInstallFinishedEventArgs> InstallFinished;

void Cancel();
void Install();
void PreparationStep();
}

public class FlutterInstallService : IFlutterInstallService
{
private readonly WebClient _webClient;
private readonly IFlutterGitClient _flutterService;

private CancellationTokenSource _cancellationSource;
private string _downloadPath = "";

public event EventHandler<FlutterInstallReadyEventArgs> InstallReady;
public event EventHandler<FlutterInstallProgressEventArgs> ProgressChanged;
public event EventHandler<FlutterInstallFinishedEventArgs> InstallFinished;

public string FlutterVersion { get; private set; }
public string InstallPath { get; set; }

public FlutterInstallService(IFlutterGitClient flutterService, WebClient webClient, IPathManager pathManager)
{
InstallPath = pathManager.GetPath(PathKind.FlutterInstallDirectory);

_flutterService = flutterService;
_webClient = webClient;
_webClient.DownloadProgressChanged += WebClient_DownloadProgressChanged;
}

#region Public Methods

public void Install()
{
_cancellationSource = new CancellationTokenSource();
ExecuteSteps();
}

public void Cancel()
{
_webClient.CancelAsync();
_cancellationSource.Cancel();
}

public void PreparationStep()
{
Task.Run(async () =>
{
try
{
FlutterVersion = await _flutterService.GetLatestFlutterVersion();
OnInstallReady(this, new FlutterInstallReadyEventArgs
{
Ready = true
});
}
catch (Exception ex)
{
OnInstallReady(this, new FlutterInstallReadyEventArgs
{
Ready = false,
ErrorMessage = ex.Message
});
}
});
}

#endregion

#region Install Steps

private void ExecuteSteps()
{
Task.Run(async () =>
{
try
{
// step 1 --- download Flutter SDK
OnProgressChanged(this, new FlutterInstallProgressEventArgs
{
ProgressPercentage = 0,
Status = "Memulai unduhan..."
});

_downloadPath = Path.GetTempFileName();
var uri = _flutterService.GetFlutterDownloadUri(FlutterVersion);

await _webClient.DownloadFileTaskAsync(new Uri(uri), _downloadPath);

// step 2 --- extract to installfolder
OnProgressChanged(this, new FlutterInstallProgressEventArgs
{
ProgressPercentage = 0,
Status = "Memulai ekstraksi..."
});

if (IsProcessCancelled()) return;
using (var zip = new ZipFile(_downloadPath))
{
var totalEntries = zip.Entries.Count;
var processedEntry = 0;
foreach (var entry in zip) // non-parallelism, the lib doesn't support parallel
{
if (IsProcessCancelled()) return;

var path = Path.GetFullPath(Path.Combine(InstallPath, "../" + entry.FileName));
if (entry.IsDirectory)
{
Directory.CreateDirectory(path);
}
else
{
using (var fs = new FileStream(path, FileMode.Create))
{
entry.Extract(fs);
}
}

Interlocked.Increment(ref processedEntry);
OnProgressChanged(this, new FlutterInstallProgressEventArgs
{
ProgressPercentage = (int)((double)processedEntry / totalEntries * 100),
Status = "Memindahkan direktori..."
});
}
}

// step 3 --- set environment variable
OnProgressChanged(this, new FlutterInstallProgressEventArgs
{
ProgressPercentage = 70,
Status = "Mengatur environment variable..."
});

if (IsProcessCancelled()) return;
var pathEnv = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User);
if (pathEnv?.Contains(@"flutter\bin") == false)
{
pathEnv += ";" + Path.Combine(InstallPath, @"bin");
Environment.SetEnvironmentVariable("PATH", pathEnv, EnvironmentVariableTarget.User);
}

// --- all done!
OnInstallFinished(this, new FlutterInstallFinishedEventArgs
{
ErrorMessage = "Instalasi selesai.",
Success = true
});
}
catch (Exception e)
{
OnInstallFinished(this, new FlutterInstallFinishedEventArgs
{
ErrorMessage = e.Message,
Success = false
});
}
});
}

private bool IsProcessCancelled()
{
if (!_cancellationSource.IsCancellationRequested) return false;
OnInstallFinished(this, new FlutterInstallFinishedEventArgs
{
ErrorMessage = "Instalasi dibatalkan.",
Success = false
});

return true;
}

#endregion

#region Event Handlers

private void WebClient_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
OnProgressChanged(this, new FlutterInstallProgressEventArgs
{
ProgressPercentage = e.ProgressPercentage,
Status = "Mengunduh..."
});
}

#endregion

#region Event Invocators

private void OnInstallReady(object sender, FlutterInstallReadyEventArgs e)
{
InstallReady?.Invoke(sender, e);
}

private void OnProgressChanged(object sender, FlutterInstallProgressEventArgs e)
{
ProgressChanged?.Invoke(sender, e);
}

private void OnInstallFinished(object sender, FlutterInstallFinishedEventArgs e)
{
InstallFinished?.Invoke(sender, e);
}

#endregion
}

public class FlutterInstallReadyEventArgs : EventArgs
{
public bool Ready { get; set; }
public string ErrorMessage { get; set; }
}

public class FlutterInstallProgressEventArgs : EventArgs
{
public int ProgressPercentage { get; set; }
public string Status { get; set; }
}

public class FlutterInstallFinishedEventArgs : EventArgs
{
public bool Success { get; set; }
public string ErrorMessage { get; set; }
}
}
3 changes: 3 additions & 0 deletions src/KFlearning/Services/KFlearningModulesInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Castle.Windsor;
using KFlearning.TemplateProvider;
using KFlearning.Views;
using System.Net;

namespace KFlearning.Services
{
Expand All @@ -11,6 +12,8 @@ public class KFlearningModulesInstaller : IWindsorInstaller
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Register(
Component.For<WebClient>().ImplementedBy<WebClient>().LifestyleTransient(),

// views
Classes.FromThisAssembly()
.InSameNamespaceAs<StartupForm>()
Expand Down
Loading

0 comments on commit aff4bee

Please sign in to comment.