Skip to content

Commit

Permalink
Merge pull request #1 from fbarresi/main
Browse files Browse the repository at this point in the history
Added Service Host project
  • Loading branch information
Peter-B- authored Dec 11, 2023
2 parents 1281a88 + d3be2f5 commit 66f9317
Show file tree
Hide file tree
Showing 15 changed files with 418 additions and 3 deletions.
64 changes: 64 additions & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# This workflow will build a .NET project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net

name: .NET

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:

runs-on: windows-latest

steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Publish ReadyToRun
run: dotnet publish ImageCompressor\ImageCompressor.csproj -c Release -r win-x64 /p:AssemblyVersion=1.0.${{ github.run_number }} /p:FileVersion=1.0.${{ github.run_number }} /p:SelfContained=true /p:PublishReadyToRun=true /p:PublishSingleFile=true -o out
- name: Compress ReadyToRun
run: Compress-Archive out\ImageCompressor.exe ImageCompressor_ReadyToRun_v1.0.${{ github.run_number }}.zip
- name: Publish Service ReadyToRun
run: dotnet publish ImageCompressor.Service\ImageCompressor.Service.csproj -c Release -r win-x64 /p:AssemblyVersion=1.0.${{ github.run_number }} /p:FileVersion=1.0.${{ github.run_number }} /p:SelfContained=true /p:PublishReadyToRun=true /p:PublishSingleFile=true -o out.Service
- name: Compress Service ReadyToRun
run: Compress-Archive out.Service\ImageCompressor.Service.exe ImageCompressor.Service_ReadyToRun_v1.0.${{ github.run_number }}.zip
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v1.0.${{ github.run_number }}
release_name: v1.0.${{ github.run_number }}
draft: false
prerelease: true
- name: Upload ReadyToRun
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
asset_path: ImageCompressor_ReadyToRun_v1.0.${{ github.run_number }}.zip
asset_name: ImageCompressor_ReadyToRun_v1.0.${{ github.run_number }}.zip
asset_content_type: application/zip
- name: Upload ReadyToRun Service
id: upload-release-asset2
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
asset_path: ImageCompressor.Service_ReadyToRun_v1.0.${{ github.run_number }}.zip
asset_name: ImageCompressor.Service_ReadyToRun_v1.0.${{ github.run_number }}.zip
asset_content_type: application/zip
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -360,4 +360,7 @@ MigrationBackup/
.ionide/

# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd

#Rider
.idea/
9 changes: 9 additions & 0 deletions ImageCompressor.Service/CompressionTasks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ImageCompressor.Service;

public class CompressionTasks
{
public string Name { get; set; }

Check warning on line 5 in ImageCompressor.Service/CompressionTasks.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 5 in ImageCompressor.Service/CompressionTasks.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 5 in ImageCompressor.Service/CompressionTasks.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
public TimeSpan InitialDelay { get; set; } = TimeSpan.FromSeconds(1);
public TimeSpan Period { get; set; } = TimeSpan.FromMinutes(5);
public CompressImagesSettings CompressionSettings { get; set; }

Check warning on line 8 in ImageCompressor.Service/CompressionTasks.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'CompressionSettings' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 8 in ImageCompressor.Service/CompressionTasks.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'CompressionSettings' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 8 in ImageCompressor.Service/CompressionTasks.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'CompressionSettings' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
}
23 changes: 23 additions & 0 deletions ImageCompressor.Service/ImageCompressor.Service.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-ImageCompressor.Service-303BD63E-8769-4D93-8BD9-EB337757F14E</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="7.0.1" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="Serilog.AspNetCore" Version="6.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="System.Reactive" Version="6.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ImageCompressor\ImageCompressor.csproj" />
</ItemGroup>
</Project>
26 changes: 26 additions & 0 deletions ImageCompressor.Service/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using ImageCompressor;
using ImageCompressor.Service;
using Microsoft.Extensions.Options;
using Serilog;

var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", true)
.Build();

IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddOptions<SettingsRoot>()
.BindConfiguration("Settings") // 👈 Bind the section
.ValidateDataAnnotations() // 👈 Enable validation
.ValidateOnStart(); // 👈 Validate on app start
services.AddSingleton(resolver => resolver.GetRequiredService<IOptions<SettingsRoot>>().Value);
})
.ConfigureServices(services => { services.AddHostedService<Worker>(); })
.UseSerilog((context, loggerConfiguration) => loggerConfiguration.ReadFrom.Configuration(configuration))
.UseWindowsService()
.Build();

host.Run();
11 changes: 11 additions & 0 deletions ImageCompressor.Service/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"profiles": {
"ImageCompressor.Service": {
"commandName": "Project",
"dotnetRunMessages": true,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}
6 changes: 6 additions & 0 deletions ImageCompressor.Service/SettingsRoot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ImageCompressor.Service;

public class SettingsRoot
{
public List<CompressionTasks> CompressionTasks { get; set; } = new ();
}
67 changes: 67 additions & 0 deletions ImageCompressor.Service/Worker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System.Reactive.Disposables;
using System.Reactive.Linq;

namespace ImageCompressor.Service;

public class Worker : BackgroundService
{
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
disposables.Dispose();
}
}

public sealed override void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

private readonly ILogger<Worker> logger;
private readonly SettingsRoot settings;

private readonly CompositeDisposable disposables = new();

public Worker(ILogger<Worker> logger, SettingsRoot settings)
{
this.logger = logger;
this.settings = settings;
}

public override Task StartAsync(CancellationToken cancellationToken)
{
foreach (var task in settings.CompressionTasks)
{
logger.LogInformation("Setting up task {TaskName}...", task.Name);
var validationResult = task.CompressionSettings.Validate();
logger.LogInformation("Validation result: {ValidationResult}: {ValidationMessage}", validationResult.Successful, validationResult.Message);
if (validationResult.Successful)
{
var subscrition = Observable.Timer(task.InitialDelay, task.Period)
.Do(_ => logger.LogInformation("Executing task {TaskName}", task.Name))
.Do(_ => (new CompressImagesCommand()).ExecuteEmbedded(logger, task.CompressionSettings))
.Retry()
.Subscribe(_ => {}, exception => logger.LogError(exception, "Error while executing task {TaskName}", task.Name));
disposables.Add(subscrition);

}
}
return base.StartAsync(cancellationToken);
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(-1, stoppingToken);
}
}

public override Task StopAsync(CancellationToken cancellationToken)
{
disposables?.Dispose();
return base.StopAsync(cancellationToken);
}
}
8 changes: 8 additions & 0 deletions ImageCompressor.Service/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
32 changes: 32 additions & 0 deletions ImageCompressor.Service/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Serilog": {
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
"MinimumLevel": "Debug",
"WriteTo": [
{ "Name": "Console" },
{ "Name": "File", "Args": { "path": "Logs/log.txt" } }
]
},
"Settings": {
"CompressionTasks": [
{
"Name": "Compress",
"CompressionSettings": {
"SourcePath": "C:\\temp\\pictures",
"TargetPath": "C:\\temp\\pictures\\out",
"DeleteOriginal": true,
"OverwriteExisting": true,
"IncludeSubDirectories": true,
"OutMode": "Jpeg",
"MinAgeInDays": 30
}
}
]
}
}
6 changes: 6 additions & 0 deletions ImageCompressor.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ VisualStudioVersion = 17.0.31808.319
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageCompressor", "ImageCompressor\ImageCompressor.csproj", "{E765A8FD-5F91-4AC8-A05E-08E4EAD5FA49}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageCompressor.Service", "ImageCompressor.Service\ImageCompressor.Service.csproj", "{C5679F38-15C9-4FDE-83AC-0B86B7458979}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -15,6 +17,10 @@ Global
{E765A8FD-5F91-4AC8-A05E-08E4EAD5FA49}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E765A8FD-5F91-4AC8-A05E-08E4EAD5FA49}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E765A8FD-5F91-4AC8-A05E-08E4EAD5FA49}.Release|Any CPU.Build.0 = Release|Any CPU
{C5679F38-15C9-4FDE-83AC-0B86B7458979}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C5679F38-15C9-4FDE-83AC-0B86B7458979}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5679F38-15C9-4FDE-83AC-0B86B7458979}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C5679F38-15C9-4FDE-83AC-0B86B7458979}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
43 changes: 42 additions & 1 deletion ImageCompressor/CompressImagesCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,44 @@
using System.IO;
using System.Linq;
using ImageCompressor.Compressors;
using Microsoft.Extensions.Logging;
using Spectre.Console.Cli;
using Spectre.Console;

namespace ImageCompressor;

internal sealed class CompressImagesCommand : Command<CompressImagesSettings>
public sealed class CompressImagesCommand : Command<CompressImagesSettings>
{
public void ExecuteEmbedded(ILogger logger, [NotNull] CompressImagesSettings settings)
{
logger?.LogInformation($"Compressing {settings.SearchPattern} files to {settings.OutMode}");
if (settings.SampleRatio is < 1)
logger?.LogInformation($"sampeling {settings.SampleRatio * 100} %");
logger?.LogInformation($"\tfrom {settings.GetSourcePath()}");
logger?.LogInformation($"\tto {settings.GetTargetPath()}");

var stopwatch = Stopwatch.StartNew();

var results = ConvertFiles(settings);

var originalSize = results.Where(r => r.Result == Result.Success).Sum(r => r.OriginalSize);
var compressedSize = results.Where(r => r.Result == Result.Success).Sum(r => r.CompressedSize);

logger?.LogInformation($"Converted {results.Count(r => r.Result == Result.Success):N0} {settings.SearchPattern} files in {stopwatch.Elapsed}.");
logger?.LogInformation($"Reduced size from {originalSize >> 20} to {(compressedSize >> 20)} MiB: {(compressedSize * 100.0 / (originalSize + 0.1)):N1} %.");
if (results.Any(r => r.Result == Result.Skipped))
{
logger?.LogInformation($"{results.Count(r => r.Result == Result.Skipped)} files already existed and were skipped.");
}
if (results.Any(r => r.Result == Result.Failed))
{
logger?.LogError($"Error: {results.Count(r => r.Result == Result.Failed)} files could not be compressed:");
foreach (var res in results.Where(r => r.Result == Result.Failed).Take(50))
{
logger?.LogInformation($"{res.Path}: {res.ErrorMessage}");
}
}
}
public override int Execute([NotNull] CommandContext context, [NotNull] CompressImagesSettings settings)
{
AnsiConsole.WriteLine();
Expand Down Expand Up @@ -93,6 +124,16 @@ private List<ConversionResult> ConvertFiles(CompressImagesSettings settings)
var files = new DirectoryInfo(settings.GetSourcePath())
.EnumerateFiles(settings.SearchPattern, settings.IncludeSubDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);

if (settings.MinAgeInDays != null)
{
files = files.Where(f => f.CreationTimeUtc <= DateTime.UtcNow.AddDays(-settings.MinAgeInDays.Value));
}

if (settings.MaxAgeInDays != null)
{
files = files.Where(f => f.CreationTimeUtc >= DateTime.UtcNow.AddDays(-settings.MaxAgeInDays.Value));
}

if (settings.SampleRatio is < 1)
{
var rand = new Random();
Expand Down
9 changes: 8 additions & 1 deletion ImageCompressor/CompressImagesSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public enum OutputMode
[Description("Number of concurrent compressions (default: [white]4[/])")]
[CommandOption("--parallel")]
[DefaultValue(4)]
public int Parallel { get; init; }
public int Parallel { get; init; } = 4;

[Description("Quality used for output\r\ndefault: [white]98[/] for Jpeg, [white]4[/] for Brotli)")]
[CommandOption("-q|--quality")]
Expand All @@ -65,6 +65,13 @@ public enum OutputMode
[Description("Path to store images. Defaults to [[sourcePath]].")]
[CommandArgument(1, "[targetPath]")]
public string? TargetPath { get; init; }

[Description("Minimal age in days of files to process - exclude files younger than n days")]
[CommandOption("--minage")]
public int? MinAgeInDays { get; init; }
[Description("Maximal age in days of files to process - exclude files older than n days")]
[CommandOption("--maxage")]
public int? MaxAgeInDays { get; init; }

public string GetSourcePath() => Path.GetFullPath(SourcePath ?? Directory.GetCurrentDirectory());
public string GetTargetPath() => Path.GetFullPath(TargetPath ?? SourcePath ?? Directory.GetCurrentDirectory());
Expand Down
1 change: 1 addition & 0 deletions ImageCompressor/ImageCompressor.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
<PackageReference Include="Spectre.Console" Version="0.45.0" />
<PackageReference Include="Spectre.Console.Cli" Version="0.45.0" />
Expand Down
Loading

0 comments on commit 66f9317

Please sign in to comment.