diff --git a/K8sFileBrowser/Assets/about.txt b/K8sFileBrowser/Assets/about.txt new file mode 100644 index 0000000..fb6bd7a --- /dev/null +++ b/K8sFileBrowser/Assets/about.txt @@ -0,0 +1,6 @@ +This favicon was generated using the following font: + +- Font Title: Genos +- Font Author: Copyright 2011 The Genos Project Authors (https://github.com/googlefonts/genos) +- Font Source: http://fonts.gstatic.com/s/genos/v10/SlGNmQqPqpUOYTYjacb0Hc91fTwVA0_orUK6K7ZsAg.ttf +- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL)) diff --git a/K8sFileBrowser/Assets/app.ico b/K8sFileBrowser/Assets/app.ico new file mode 100644 index 0000000..1e6df25 Binary files /dev/null and b/K8sFileBrowser/Assets/app.ico differ diff --git a/K8sFileBrowser/Assets/avalonia-logo.ico b/K8sFileBrowser/Assets/avalonia-logo.ico deleted file mode 100644 index da8d49f..0000000 Binary files a/K8sFileBrowser/Assets/avalonia-logo.ico and /dev/null differ diff --git a/K8sFileBrowser/K8sFileBrowser.csproj b/K8sFileBrowser/K8sFileBrowser.csproj index 4cbeefe..d8f75d4 100644 --- a/K8sFileBrowser/K8sFileBrowser.csproj +++ b/K8sFileBrowser/K8sFileBrowser.csproj @@ -8,6 +8,7 @@ true Debug;Release AnyCPU + Assets/app.ico TRACE @@ -28,7 +29,9 @@ + + diff --git a/K8sFileBrowser/Program.cs b/K8sFileBrowser/Program.cs index 4388ae3..8117845 100644 --- a/K8sFileBrowser/Program.cs +++ b/K8sFileBrowser/Program.cs @@ -16,10 +16,10 @@ class Program { Log.Logger = new LoggerConfiguration() //.Filter.ByIncludingOnly(Matching.WithProperty("Area", LogArea.Control)) - .MinimumLevel.Verbose() - .WriteTo.Console() + .MinimumLevel.Information() + .WriteTo.Async(a => a.File("app.log")) .CreateLogger(); - + BuildAvaloniaApp() .StartWithClassicDesktopLifetime(args); } @@ -30,7 +30,7 @@ class Program } finally { - // This block is optional. + // This block is optional. // Use the finally-block if you need to clean things up or similar Log.CloseAndFlush(); } diff --git a/K8sFileBrowser/Services/IKubernetesService.cs b/K8sFileBrowser/Services/IKubernetesService.cs new file mode 100644 index 0000000..82be887 --- /dev/null +++ b/K8sFileBrowser/Services/IKubernetesService.cs @@ -0,0 +1,20 @@ +// // Copyright (c) Vector Informatik GmbH. All rights reserved. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using K8sFileBrowser.Models; + +namespace K8sFileBrowser.Services; + +public interface IKubernetesService +{ + IEnumerable GetClusterContexts(); + string GetCurrentContext(); + void SwitchClusterContext(ClusterContext clusterContext); + IEnumerable GetNamespaces(); + IEnumerable GetPods(string namespaceName); + IList GetFiles(string namespaceName, string podName, string containerName, string path); + Task DownloadFile(Namespace? selectedNamespace, Pod? selectedPod, FileInformation selectedFile, + string? saveFileName, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/K8sFileBrowser/Services/KubernetesService.cs b/K8sFileBrowser/Services/KubernetesService.cs index 6dfb0b4..bc7e8dd 100644 --- a/K8sFileBrowser/Services/KubernetesService.cs +++ b/K8sFileBrowser/Services/KubernetesService.cs @@ -14,7 +14,7 @@ using static ICSharpCode.SharpZipLib.Core.StreamUtils; namespace K8sFileBrowser.Services; -public class KubernetesService +public class KubernetesService : IKubernetesService { private readonly K8SConfiguration _k8SConfiguration; private IKubernetes _kubernetesClient = null!; @@ -98,11 +98,10 @@ public class KubernetesService _kubernetesClient = kubernetesClient; } - public async Task DownloadFile(Namespace? selectedNamespace, Pod? selectedPod, FileInformation selectedFile, - string? saveFileName) + string? saveFileName, CancellationToken cancellationToken = default) { - Log.Information("{SelectedNamespace} - {SelectedPod} - {SelectedFile} - {SaveFileName}", + Log.Information("{SelectedNamespace} - {SelectedPod} - {SelectedFile} - {SaveFileName}", selectedNamespace, selectedPod, selectedFile, saveFileName); var handler = new ExecAsyncCallback(async (_, stdOut, stdError) => { @@ -111,15 +110,15 @@ public class KubernetesService await using var outputFileStream = File.OpenWrite(saveFileName!); await using var tarInputStream = new TarInputStream(stdOut, Encoding.Default); - var entry = await tarInputStream.GetNextEntryAsync(default); + var entry = await tarInputStream.GetNextEntryAsync(cancellationToken); if (entry == null) { throw new IOException("Copy command failed: no files found"); } - + var bytes = new byte[entry.Size]; ReadFully( tarInputStream, bytes ); - await outputFileStream.WriteAsync(bytes, default); + await outputFileStream.WriteAsync(bytes, cancellationToken); } catch (Exception ex) { @@ -129,7 +128,7 @@ public class KubernetesService using var streamReader = new StreamReader(stdError); while (streamReader.EndOfStream == false) { - var error = await streamReader.ReadToEndAsync(default); + var error = await streamReader.ReadToEndAsync(cancellationToken); Log.Error(error); } }); diff --git a/K8sFileBrowser/ViewModels/MainWindowViewModel.cs b/K8sFileBrowser/ViewModels/MainWindowViewModel.cs index d0bbfc2..000d4dc 100644 --- a/K8sFileBrowser/ViewModels/MainWindowViewModel.cs +++ b/K8sFileBrowser/ViewModels/MainWindowViewModel.cs @@ -11,154 +11,161 @@ namespace K8sFileBrowser.ViewModels; public class MainWindowViewModel : ViewModelBase { - private readonly ObservableAsPropertyHelper> _clusterContexts; - public IEnumerable ClusterContexts => _clusterContexts.Value; + private readonly ObservableAsPropertyHelper> _clusterContexts; + public IEnumerable ClusterContexts => _clusterContexts.Value; - private ClusterContext? _selectedClusterContext; - public ClusterContext? SelectedClusterContext + private ClusterContext? _selectedClusterContext; + + public ClusterContext? SelectedClusterContext + { + get => _selectedClusterContext; + set => this.RaiseAndSetIfChanged(ref _selectedClusterContext, value); + } + + private readonly ObservableAsPropertyHelper> _namespaces; + public IEnumerable Namespaces => _namespaces.Value; + + private Namespace? _selectedNamespace; + + public Namespace? SelectedNamespace + { + get => _selectedNamespace; + set => this.RaiseAndSetIfChanged(ref _selectedNamespace, value); + } + + private readonly ObservableAsPropertyHelper> _pods; + public IEnumerable Pods => _pods.Value; + + private Pod? _selectedPod; + + public Pod? SelectedPod + { + get => _selectedPod; + set => this.RaiseAndSetIfChanged(ref _selectedPod, value); + } + + private readonly ObservableAsPropertyHelper> _fileInformation; + public IEnumerable FileInformation => _fileInformation.Value; + + private FileInformation? _selectedFile; + + public FileInformation? SelectedFile + { + get => _selectedFile; + set => this.RaiseAndSetIfChanged(ref _selectedFile, value); + } + + private string? _selectedPath; + + public string? SelectedPath + { + get => _selectedPath; + set => this.RaiseAndSetIfChanged(ref _selectedPath, value); + } + + private bool _isDownloadActive; + + public bool IsDownloadActive + { + get => _isDownloadActive; + set => this.RaiseAndSetIfChanged(ref _isDownloadActive, value); + } + + public ReactiveCommand DownloadCommand { get; } + public ReactiveCommand ParentCommand { get; } + public ReactiveCommand OpenCommand { get; } + + + public MainWindowViewModel() + { + //TODO: use dependency injection to get the kubernetes service + IKubernetesService kubernetesService = new KubernetesService(); + + var isFile = this + .WhenAnyValue(x => x.SelectedFile, x => x.IsDownloadActive) + .Select(x => x is { Item1.Type: FileType.File, Item2: false }); + + var isDirectory = this + .WhenAnyValue(x => x.SelectedFile, x => x.IsDownloadActive) + .Select(x => x is { Item1.Type: FileType.Directory, Item2: false }); + + var isNotRoot = this + .WhenAnyValue(x => x.SelectedPath, x => x.IsDownloadActive) + .Select(x => x.Item1 is not "/" && !x.Item2); + + OpenCommand = ReactiveCommand.Create(() => { SelectedPath = SelectedFile != null ? SelectedFile!.Name : "/"; }, + isDirectory, RxApp.MainThreadScheduler); + + DownloadCommand = ReactiveCommand.CreateFromTask(async () => { - get => _selectedClusterContext; - set => this.RaiseAndSetIfChanged(ref _selectedClusterContext, value); - } - - private readonly ObservableAsPropertyHelper> _namespaces; - public IEnumerable Namespaces => _namespaces.Value; - - private Namespace? _selectedNamespace; - public Namespace? SelectedNamespace - { - get => _selectedNamespace; - set => this.RaiseAndSetIfChanged(ref _selectedNamespace, value); - } - - private readonly ObservableAsPropertyHelper> _pods; - public IEnumerable Pods => _pods.Value; - - private Pod? _selectedPod; - public Pod? SelectedPod - { - get => _selectedPod; - set => this.RaiseAndSetIfChanged(ref _selectedPod, value); - } - - private readonly ObservableAsPropertyHelper> _fileInformation; - public IEnumerable FileInformation => _fileInformation.Value; - - private FileInformation? _selectedFile; - public FileInformation? SelectedFile - { - get => _selectedFile; - set => this.RaiseAndSetIfChanged(ref _selectedFile, value); - } - - private string? _selectedPath; - public string? SelectedPath - { - get => _selectedPath; - set => this.RaiseAndSetIfChanged(ref _selectedPath, value); - } - - private bool _isDownloadActive; - public bool IsDownloadActive - { - get => _isDownloadActive; - set => this.RaiseAndSetIfChanged(ref _isDownloadActive, value); - } - - public ReactiveCommand DownloadCommand { get; } - public ReactiveCommand ParentCommand { get; } - public ReactiveCommand OpenCommand { get; } - - - public MainWindowViewModel() - { - var kubernetesService = new KubernetesService(); - - var isFile = this - .WhenAnyValue(x => x.SelectedFile, x => x.IsDownloadActive) - .Select(x => x is { Item1.Type: FileType.File, Item2: false }); - - var isDirectory = this - .WhenAnyValue(x => x.SelectedFile, x => x.IsDownloadActive) - .Select(x => x is { Item1.Type: FileType.Directory, Item2: false }); - - var isNotRoot = this - .WhenAnyValue(x => x.SelectedPath, x => x.IsDownloadActive) - .Select(x => x.Item1 is not "/" && !x.Item2); - - OpenCommand = ReactiveCommand.Create(() => + await Observable.StartAsync(async () => + { + var fileName = SelectedFile!.Name.Substring(SelectedFile!.Name.LastIndexOf('/') + 1, + SelectedFile!.Name.Length - SelectedFile!.Name.LastIndexOf('/') - 1); + var saveFileName = await ApplicationHelper.SaveFile(".", fileName); + if (saveFileName != null) { - SelectedPath = SelectedFile != null ? SelectedFile!.Name : "/"; - }, isDirectory, RxApp.MainThreadScheduler); + IsDownloadActive = true; + await kubernetesService.DownloadFile(SelectedNamespace, SelectedPod, SelectedFile, saveFileName); + IsDownloadActive = false; + } + }, RxApp.TaskpoolScheduler); + }, isFile, RxApp.MainThreadScheduler); - DownloadCommand = ReactiveCommand.CreateFromTask(async () => - { - await Observable.StartAsync(async () => { - var fileName = SelectedFile!.Name.Substring(SelectedFile!.Name.LastIndexOf('/') + 1, SelectedFile!.Name.Length - SelectedFile!.Name.LastIndexOf('/') - 1); - var saveFileName = await ApplicationHelper.SaveFile(".", fileName); - if (saveFileName != null) - { - IsDownloadActive = true; - await kubernetesService.DownloadFile(SelectedNamespace, SelectedPod, SelectedFile, saveFileName); - IsDownloadActive = false; - } - }, RxApp.TaskpoolScheduler); - }, isFile, RxApp.MainThreadScheduler); + ParentCommand = ReactiveCommand.Create(() => + { + SelectedPath = SelectedPath![..SelectedPath!.LastIndexOf('/')]; + if (SelectedPath!.Length == 0) + { + SelectedPath = "/"; + } + }, isNotRoot, RxApp.MainThreadScheduler); - ParentCommand = ReactiveCommand.Create(() => - { - SelectedPath = SelectedPath![..SelectedPath!.LastIndexOf('/')]; - if (SelectedPath!.Length == 0) - { - SelectedPath = "/"; - } - }, isNotRoot, RxApp.MainThreadScheduler); + // read the cluster contexts + _namespaces = this + .WhenAnyValue(c => c.SelectedClusterContext) + .Throttle(TimeSpan.FromMilliseconds(10)) + .Where(context => context != null) + .Select(context => + { + kubernetesService.SwitchClusterContext(context!); + return kubernetesService.GetNamespaces(); + }) + .ObserveOn(RxApp.MainThreadScheduler) + .ToProperty(this, x => x.Namespaces); - // read the cluster contexts - _namespaces = this - .WhenAnyValue(c => c.SelectedClusterContext) - .Throttle(TimeSpan.FromMilliseconds(10)) - .Where(context => context != null) - .Select(context => - { - kubernetesService.SwitchClusterContext(context!); - return kubernetesService.GetNamespaces(); - }) - .ObserveOn(RxApp.MainThreadScheduler) - .ToProperty(this, x => x.Namespaces); + // read the pods when the namespace changes + _pods = this + .WhenAnyValue(c => c.SelectedNamespace) + .Throttle(TimeSpan.FromMilliseconds(10)) + .Where(ns => ns != null) + .Select(ns => kubernetesService.GetPods(ns!.Name)) + .ObserveOn(RxApp.MainThreadScheduler) + .ToProperty(this, x => x.Pods); - // read the pods when the namespace changes - _pods = this - .WhenAnyValue(c => c.SelectedNamespace) - .Throttle(TimeSpan.FromMilliseconds(10)) - .Where(ns => ns != null) - .Select(ns => kubernetesService.GetPods(ns!.Name)) - .ObserveOn(RxApp.MainThreadScheduler) - .ToProperty(this, x => x.Pods); + // read the file information when the path changes + _fileInformation = this + .WhenAnyValue(c => c.SelectedPath, c => c.SelectedPod, c => c.SelectedNamespace) + .Throttle(TimeSpan.FromMilliseconds(10)) + .Select(x => x.Item3 == null || x.Item2 == null + ? new List() + : kubernetesService.GetFiles(x.Item3!.Name, x.Item2!.Name, x.Item2!.Containers.First(), + x.Item1)) + .ObserveOn(RxApp.MainThreadScheduler) + .ToProperty(this, x => x.FileInformation); - // read the file information when the path changes - _fileInformation = this - .WhenAnyValue(c => c.SelectedPath, c => c.SelectedPod, c => c.SelectedNamespace) - .Throttle(TimeSpan.FromMilliseconds(10)) - .Select(x => x.Item3 == null || x.Item2 == null - ? new List() - : kubernetesService.GetFiles(x.Item3!.Name, x.Item2!.Name, x.Item2!.Containers.First(), - x.Item1)) - .ObserveOn(RxApp.MainThreadScheduler) - .ToProperty(this, x => x.FileInformation); + // reset the path when the pod or namespace changes + this.WhenAnyValue(c => c.SelectedPod, c => c.SelectedNamespace) + .Subscribe(x => SelectedPath = "/"); - // reset the path when the pod or namespace changes - this.WhenAnyValue(c => c.SelectedPod, c => c.SelectedNamespace) - .Subscribe(x => SelectedPath = "/"); + // load the cluster contexts when the view model is created + var loadContexts = ReactiveCommand + .Create>(_ => kubernetesService.GetClusterContexts()); + _clusterContexts = loadContexts.Execute().ToProperty( + this, x => x.ClusterContexts, scheduler: RxApp.MainThreadScheduler); - // load the cluster contexts when the view model is created - var loadContexts = ReactiveCommand - .Create>(_ => kubernetesService.GetClusterContexts()); - _clusterContexts = loadContexts.Execute().ToProperty( - this, x => x.ClusterContexts, scheduler: RxApp.MainThreadScheduler); - - // select the current cluster context - SelectedClusterContext = ClusterContexts - .FirstOrDefault(x => x.Name == kubernetesService.GetCurrentContext()); - } + // select the current cluster context + SelectedClusterContext = ClusterContexts + .FirstOrDefault(x => x.Name == kubernetesService.GetCurrentContext()); + } } \ No newline at end of file diff --git a/K8sFileBrowser/Views/MainWindow.axaml b/K8sFileBrowser/Views/MainWindow.axaml index 5af3685..f657808 100644 --- a/K8sFileBrowser/Views/MainWindow.axaml +++ b/K8sFileBrowser/Views/MainWindow.axaml @@ -7,7 +7,7 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="K8sFileBrowser.Views.MainWindow" x:DataType="vm:MainWindowViewModel" - Icon="/Assets/avalonia-logo.ico" + Icon="/Assets/app.ico" Title="K8sFileBrowser"> @@ -77,7 +77,7 @@ - +