Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Client.Wasm/Components/StudentCard.razor
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
</CardHeader>
<CardBody>
<UnorderedList Unstyled>
<UnorderedListItem>Номер <Strong>№X "Название лабораторной"</Strong></UnorderedListItem>
<UnorderedListItem>Вариант <Strong>№Х "Название варианта"</Strong></UnorderedListItem>
<UnorderedListItem>Выполнена <Strong>Фамилией Именем 65ХХ</Strong> </UnorderedListItem>
<UnorderedListItem><Link To="https://puginarug.com/">Ссылка на форк</Link></UnorderedListItem>
<UnorderedListItem>Номер <Strong>№3 "Интеграционное тестирование"</Strong></UnorderedListItem>
<UnorderedListItem>Вариант <Strong>№47 "Транспортное средство"</Strong></UnorderedListItem>
<UnorderedListItem>Выполнена <Strong>Морозов Сергей Сергеевич 6513</Strong> </UnorderedListItem>
<UnorderedListItem><Link To="https://github.com/kingtuler1454/cloud-development">Ссылка на форк</Link></UnorderedListItem>
</UnorderedList>
</CardBody>
</Card>
6 changes: 3 additions & 3 deletions Client.Wasm/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchBrowser": false,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5127",
"environmentVariables": {
Expand All @@ -22,7 +22,7 @@
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchBrowser": false,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:7282;http://localhost:5127",
"environmentVariables": {
Expand All @@ -31,7 +31,7 @@
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchBrowser": false,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
Expand Down
2 changes: 1 addition & 1 deletion Client.Wasm/wwwroot/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
}
},
"AllowedHosts": "*",
"BaseAddress": ""
"BaseAddress": "https://localhost:7163/vehicles"
}
36 changes: 36 additions & 0 deletions CloudDevelopment.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ VisualStudioVersion = 17.14.36811.4
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Wasm", "Client.Wasm\Client.Wasm.csproj", "{AE7EEA74-2FE0-136F-D797-854FD87E022A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VehicleApp.AppHost", "VehicleApp\VehicleApp.AppHost\VehicleApp.AppHost.csproj", "{5A32E624-1368-47E1-AE8C-C5D666C99826}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VehicleApp.ServiceDefaults", "VehicleApp\VehicleApp.ServiceDefaults\VehicleApp.ServiceDefaults.csproj", "{97B1F7F0-C53D-2D4D-3803-B1150F873870}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VehicleApp.Api", "VehicleApp.Api\VehicleApp.Api.csproj", "{54C02CF3-616B-EC3E-6F35-EAF0ED92B6B0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VehicleApp.Gateway", "VehicleApp.Gateway\VehicleApp.Gateway.csproj", "{CBFDC59A-3449-C88A-3139-3F56883062A4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VehicleApp.AppHost.Tests", "VehicleApp.AppHost.Tests\VehicleApp.AppHost.Tests.csproj", "{D88DD2ED-1E2A-4997-80D5-BE76658A927B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "File.Service", "File.Service\File.Service.csproj", "{DF944664-05C9-E01F-B262-C8B381C1EFE1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -15,6 +27,30 @@ Global
{AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.Build.0 = Release|Any CPU
{5A32E624-1368-47E1-AE8C-C5D666C99826}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5A32E624-1368-47E1-AE8C-C5D666C99826}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A32E624-1368-47E1-AE8C-C5D666C99826}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5A32E624-1368-47E1-AE8C-C5D666C99826}.Release|Any CPU.Build.0 = Release|Any CPU
{97B1F7F0-C53D-2D4D-3803-B1150F873870}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{97B1F7F0-C53D-2D4D-3803-B1150F873870}.Debug|Any CPU.Build.0 = Debug|Any CPU
{97B1F7F0-C53D-2D4D-3803-B1150F873870}.Release|Any CPU.ActiveCfg = Release|Any CPU
{97B1F7F0-C53D-2D4D-3803-B1150F873870}.Release|Any CPU.Build.0 = Release|Any CPU
{54C02CF3-616B-EC3E-6F35-EAF0ED92B6B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{54C02CF3-616B-EC3E-6F35-EAF0ED92B6B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{54C02CF3-616B-EC3E-6F35-EAF0ED92B6B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{54C02CF3-616B-EC3E-6F35-EAF0ED92B6B0}.Release|Any CPU.Build.0 = Release|Any CPU
{CBFDC59A-3449-C88A-3139-3F56883062A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CBFDC59A-3449-C88A-3139-3F56883062A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CBFDC59A-3449-C88A-3139-3F56883062A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CBFDC59A-3449-C88A-3139-3F56883062A4}.Release|Any CPU.Build.0 = Release|Any CPU
{D88DD2ED-1E2A-4997-80D5-BE76658A927B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D88DD2ED-1E2A-4997-80D5-BE76658A927B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D88DD2ED-1E2A-4997-80D5-BE76658A927B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D88DD2ED-1E2A-4997-80D5-BE76658A927B}.Release|Any CPU.Build.0 = Release|Any CPU
{DF944664-05C9-E01F-B262-C8B381C1EFE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DF944664-05C9-E01F-B262-C8B381C1EFE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DF944664-05C9-E01F-B262-C8B381C1EFE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DF944664-05C9-E01F-B262-C8B381C1EFE1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
47 changes: 47 additions & 0 deletions File.Service/Controllers/S3Controller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Text.Json.Nodes;
using File.Service.Storage;
using Microsoft.AspNetCore.Mvc;

namespace File.Service.Controllers;

/// <summary>
/// HTTP-интерфейс на чтение содержимого бакета Minio.
/// Используется интеграционными тестами и для ручной проверки состояния хранилища
/// </summary>
/// <param name="s3Service">Сервис доступа к бакету</param>
/// <param name="logger">Логгер HTTP-обращений</param>
[ApiController]
[Route("api/s3")]
public class S3Controller(IS3Service s3Service, ILogger<S3Controller> logger) : ControllerBase
{
/// <summary>
/// Возвращает список ключей всех объектов в бакете
/// </summary>
[HttpGet]
public async Task<ActionResult<List<string>>> ListFiles()
{
logger.LogInformation("ListFiles was called");
var list = await s3Service.GetFileList();
return Ok(list);
}

/// <summary>
/// Возвращает содержимое объекта по ключу. Если объекта нет — 404
/// </summary>
/// <param name="key">Ключ объекта в бакете, например <c>vehicle_42.json</c></param>
[HttpGet("{key}")]
public async Task<ActionResult<JsonNode>> GetFile(string key)
{
logger.LogInformation("GetFile was called for {key}", key);
try
{
var node = await s3Service.DownloadFile(key);
return Ok(node);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to download {key}", key);
return NotFound();
}
}
}
69 changes: 69 additions & 0 deletions File.Service/Controllers/SnsWebhookController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Text;
using Amazon.SimpleNotificationService.Util;
using File.Service.Storage;
using Microsoft.AspNetCore.Mvc;

namespace File.Service.Controllers;

/// <summary>
/// Эндпоинт, на который SNS отправляет запросы подтверждения подписки и уведомления
/// </summary>
/// <param name="s3Service">Сервис записи в Minio</param>
/// <param name="logger">Логгер входящих сообщений</param>
[ApiController]
[Route("api/sns")]
public class SnsWebhookController(IS3Service s3Service, ILogger<SnsWebhookController> logger) : ControllerBase
{
/// <summary>
/// Обрабатывает тело SNS-запроса. Для <c>SubscriptionConfirmation</c> выполняет GET
/// по <c>SubscribeURL</c> (с переписыванием адреса на LocalStack). Для <c>Notification</c>
/// передаёт сообщение в <see cref="IS3Service.UploadFile"/>. Всегда отвечает 200,
/// чтобы SNS не помечал подписчика как недоступного
/// </summary>
[HttpPost]
public async Task<IActionResult> ReceiveMessage()
{
logger.LogInformation("SNS webhook was called");
using var reader = new StreamReader(Request.Body, Encoding.UTF8);
var body = await reader.ReadToEndAsync();

try
{
var message = Message.ParseMessage(body);

if (message.Type == "SubscriptionConfirmation")
{
logger.LogInformation("Received SubscriptionConfirmation, confirming");
using var http = new HttpClient();
var builder = new UriBuilder(new Uri(message.SubscribeURL))
{
Scheme = "http",
Host = "localhost",
Port = 4566
};
var confirmation = await http.GetAsync(builder.Uri);
if (!confirmation.IsSuccessStatusCode)
{
var text = await confirmation.Content.ReadAsStringAsync();
logger.LogError("SubscriptionConfirmation returned {code}: {body}", confirmation.StatusCode, text);
}
else
{
logger.LogInformation("Subscription confirmed");
}
return Ok();
}

if (message.Type == "Notification")
{
await s3Service.UploadFile(message.MessageText);
logger.LogInformation("Notification was processed and uploaded to Minio");
}
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to process SNS message");
}
return Ok();
}
}
23 changes: 23 additions & 0 deletions File.Service/File.Service.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AWSSDK.SimpleNotificationService" Version="4.0.2.17" />
<PackageReference Include="CommunityToolkit.Aspire.Minio.Client" Version="9.9.0" />
<PackageReference Include="LocalStack.Client" Version="2.0.0" />
<PackageReference Include="LocalStack.Client.Extensions" Version="2.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.4" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\VehicleApp\VehicleApp.ServiceDefaults\VehicleApp.ServiceDefaults.csproj" />
</ItemGroup>

</Project>
43 changes: 43 additions & 0 deletions File.Service/Messaging/SnsSubscriptionService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Net;
using Amazon.SimpleNotificationService;
using Amazon.SimpleNotificationService.Model;

namespace File.Service.Messaging;

/// <summary>
/// На старте приложения регистрирует HTTP-вебхук File.Service в SNS-топике
/// </summary>
/// <param name="snsClient">SNS-клиент для вызова <c>Subscribe</c></param>
/// <param name="configuration">Источник ARN топика и URL подписчика</param>
/// <param name="logger">Логгер</param>
public class SnsSubscriptionService(
IAmazonSimpleNotificationService snsClient,
IConfiguration configuration,
ILogger<SnsSubscriptionService> logger)
{
private readonly string _topicArn = configuration["AWS:Resources:SNSTopicArn"]
?? throw new KeyNotFoundException("SNS topic ARN was not found in configuration");
private readonly string _endpoint = configuration["AWS:Resources:SNSUrl"]
?? throw new KeyNotFoundException("SNS subscriber endpoint was not found in configuration");

/// <summary>
/// Отправляет в SNS запрос <c>Subscribe</c>. Подтверждение подписки
/// выполняется позже в <see cref="Controllers.SnsWebhookController"/>
/// </summary>
public async Task SubscribeEndpoint()
{
logger.LogInformation("Subscribing endpoint {endpoint} to topic {topic}", _endpoint, _topicArn);
var request = new SubscribeRequest
{
TopicArn = _topicArn,
Protocol = "http",
Endpoint = _endpoint,
ReturnSubscriptionArn = true
};
var response = await snsClient.SubscribeAsync(request);
if (response.HttpStatusCode != HttpStatusCode.OK)
logger.LogError("Failed to subscribe to {topic}: {code}", _topicArn, response.HttpStatusCode);
else
logger.LogInformation("Subscription request for {topic} accepted; awaiting confirmation", _topicArn);
}
}
44 changes: 44 additions & 0 deletions File.Service/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Amazon.SimpleNotificationService;
using File.Service.Messaging;
using File.Service.Storage;
using LocalStack.Client.Extensions;
using System.Reflection;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();
builder.Services.AddSwaggerGen(options =>
{
var assembly = Assembly.GetExecutingAssembly();
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, $"{assembly.GetName().Name}.xml"));
});

builder.Services.AddControllers();

builder.Services.AddLocalStack(builder.Configuration);
builder.Services.AddAwsService<IAmazonSimpleNotificationService>();
builder.Services.AddScoped<SnsSubscriptionService>();

builder.AddMinioClient("vehicle-minio");
builder.Services.AddScoped<IS3Service, MinioS3Service>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.MapDefaultEndpoints();

using var scope = app.Services.CreateScope();

var s3 = scope.ServiceProvider.GetRequiredService<IS3Service>();
await s3.EnsureBucketExists();
var subscription = scope.ServiceProvider.GetRequiredService<SnsSubscriptionService>();
await subscription.SubscribeEndpoint();

Comment thread
alxmcs marked this conversation as resolved.

app.MapControllers();
app.Run();
31 changes: 31 additions & 0 deletions File.Service/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5280",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"launchUrl": "swagger"
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7017;http://localhost:5280",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"launchUrl": "swagger"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
32 changes: 32 additions & 0 deletions File.Service/Storage/IS3Service.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Text.Json.Nodes;

namespace File.Service.Storage;

/// <summary>
/// Сервис работы с Minio-бакетом, в который сохраняются ТС из SNS-уведомлений
/// </summary>
public interface IS3Service
{
/// <summary>
/// Создаёт бакет, если его ещё нет
/// </summary>
Task EnsureBucketExists();

/// <summary>
/// Сохраняет JSON-тело SNS-сообщения в бакет под ключом <c>vehicle_{id}.json</c>
/// </summary>
/// <param name="fileData">Сериализованное ТС</param>
/// <returns><c>true</c> при успешной загрузке</returns>
Task<bool> UploadFile(string fileData);

/// <summary>
/// Возвращает список ключей всех объектов в бакете
/// </summary>
Task<List<string>> GetFileList();

/// <summary>
/// Читает объект из бакета и парсит его как JSON
/// </summary>
/// <param name="key">Ключ объекта</param>
Task<JsonNode> DownloadFile(string key);
}
Loading