< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.Controllers.AssemblyListController
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/AssemblyListController.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 373
Coverable lines: 373
Total lines: 518
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 124
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
.ctor(...)100%210%
Index()0%620%
Generate()0%272160%
Progress()0%272160%
Result(...)0%620%
DownloadExcel(...)0%506220%
DownloadPdf()0%4260%
History()100%210%
HistoryDetail()0%4260%
DownloadHistoryExcel()0%506220%
DownloadHistoryPdf()0%7280%
LabelProxy()0%110100%
ImageProxy()0%110100%
WriteSSE()100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/AssemblyListController.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2using System.Text;
 3using System.Text.Json;
 4using Microsoft.AspNetCore.Authorization;
 5using Microsoft.AspNetCore.Mvc;
 6using Microsoft.Extensions.Caching.Memory;
 7using OfficeOpenXml;
 8using OfficeOpenXml.Style;
 9using QuestPDF.Fluent;
 10using QuestPDF.Helpers;
 11using QuestPDF.Infrastructure;
 12using Services.ApiServices.OzonApi;
 13using Services.DataServices;
 14using OzonOrdersWeb.Areas.Studio2.ViewModels.AssemblyList;
 15
 16namespace OzonOrdersWeb.Areas.Studio2.Controllers;
 17
 18[Authorize(Roles = "User,Admin")]
 19[Area("Studio2")]
 20public class AssemblyListController : Controller
 21{
 022    private static readonly JsonSerializerOptions CamelCaseOptions = new()
 023    {
 024        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 025    };
 26
 27    private readonly OzonClientServices _ozonClientServices;
 28    private readonly IHttpClientFactory _httpClientFactory;
 29    private readonly IMemoryCache _cache;
 30    private readonly AssemblyListRunRepository _runRepository;
 31    private readonly AssemblyListGenerationOrchestrator _orchestrator;
 32
 033    public AssemblyListController(
 034        OzonClientServices ozonClientServices,
 035        IHttpClientFactory httpClientFactory,
 036        IMemoryCache cache,
 037        AssemblyListRunRepository runRepository,
 038        AssemblyListGenerationOrchestrator orchestrator)
 39    {
 040        _ozonClientServices = ozonClientServices;
 041        _httpClientFactory = httpClientFactory;
 042        _cache = cache;
 043        _runRepository = runRepository;
 044        _orchestrator = orchestrator;
 045    }
 46
 47    [HttpGet]
 48    public async Task<IActionResult> Index()
 49    {
 050        var clients = await _ozonClientServices.GetOzonClients();
 051        var lastRun = await _runRepository.GetLastCompletedRunAsync();
 052        var lastRunItems = lastRun != null
 053            ? await _runRepository.GetRunItemsAsync(lastRun.Id)
 054            : new List<OzonDomains.Models.AssemblyListRunItem>();
 55
 056        return View(new AssemblyListIndexViewModel
 057        {
 058            Clients = clients,
 059            LastRun = lastRun,
 060            LastRunItems = lastRunItems,
 061        });
 062    }
 63
 64    [HttpPost]
 65    public async Task<IActionResult> Generate([FromBody] AssemblyListFilterDto filter)
 66    {
 067        if (filter.OzonClientId <= 0)
 068            return BadRequest(new { error = "Выберите клиента" });
 69
 070        var client = await _ozonClientServices.GetOzonClientAsync(filter.OzonClientId);
 071        if (client?.DecryptClientId == null || client.DecryptApiKey == null)
 072            return BadRequest(new { error = "Клиент не найден или нет API-ключей" });
 73
 074        var req = new GenerationRequest(
 075            OzonClientId: filter.OzonClientId,
 076            OzonClientName: client.Name ?? client.DecryptClientId,
 077            DecryptedClientId: client.DecryptClientId,
 078            DecryptedApiKey: client.DecryptApiKey,
 079            CutoffFrom: filter.CutoffFrom ?? DateTime.Today,
 080            CutoffTo: filter.CutoffTo ?? DateTime.Today.AddDays(7),
 081            Status: filter.Status ?? "awaiting_deliver",
 082            WarehouseFilter: null,
 083            PersistItemsInBackground: true);
 84
 085        var (jobId, dbRunId) = await _orchestrator.StartAsync(req);
 086        return Ok(new { jobId, dbRunId });
 087    }
 88
 89    [HttpGet]
 90    public async Task Progress(string jobId)
 91    {
 092        Response.Headers["Content-Type"] = "text/event-stream";
 093        Response.Headers["Cache-Control"] = "no-cache";
 094        Response.Headers["Connection"] = "keep-alive";
 95
 096        var timeout = DateTime.UtcNow.AddMinutes(10);
 97
 098        ConcurrentQueue<AssemblyListProgress>? queue = null;
 099        for (var attempt = 0; attempt < 10 && queue == null; attempt++)
 100        {
 0101            queue = _cache.Get<ConcurrentQueue<AssemblyListProgress>>($"al_progress_{jobId}");
 0102            if (queue == null)
 0103                await Task.Delay(500);
 104        }
 105
 0106        if (queue == null)
 107        {
 0108            await WriteSSE(Response, "progress",
 0109                JsonSerializer.Serialize(new AssemblyListProgress { IsError = true, ErrorMessage = "Job not found" }, Ca
 0110            await Response.Body.FlushAsync();
 0111            return;
 112        }
 113
 0114        while (DateTime.UtcNow < timeout)
 115        {
 0116            while (queue.TryDequeue(out var prog))
 117            {
 0118                var json = JsonSerializer.Serialize(prog, CamelCaseOptions);
 0119                await WriteSSE(Response, "progress", json);
 0120                await Response.Body.FlushAsync();
 121
 0122                if (prog.IsComplete || prog.IsError)
 0123                    return;
 124            }
 125
 0126            await Task.Delay(400);
 127        }
 0128    }
 129
 130    [HttpGet]
 131    public IActionResult Result(string jobId)
 132    {
 0133        var result = _cache.Get<AssemblyListResult>($"al_result_{jobId}");
 0134        if (result == null)
 0135            return NotFound(new { error = "Данные не найдены или истёк срок хранения" });
 136
 0137        return Content(JsonSerializer.Serialize(result, CamelCaseOptions), "application/json");
 138    }
 139
 140    [HttpGet]
 141    public IActionResult DownloadExcel(string jobId)
 142    {
 0143        var result = _cache.Get<AssemblyListResult>($"al_result_{jobId}");
 0144        if (result == null)
 0145            return NotFound("Данные не найдены или истёк срок хранения");
 146
 0147        ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
 0148        using var package = new ExcelPackage();
 0149        var ws = package.Workbook.Worksheets.Add("Лист подбора");
 150
 0151        var headers = new[] { "№", "Отправление", "Артикул", "Название", "Склад", "Статус", "Кол-во", "Дедлайн", "Постав
 0152        for (var col = 1; col <= headers.Length; col++)
 153        {
 0154            ws.Cells[1, col].Value = headers[col - 1];
 0155            ws.Cells[1, col].Style.Font.Bold = true;
 0156            ws.Cells[1, col].Style.Fill.PatternType = ExcelFillStyle.Solid;
 0157            ws.Cells[1, col].Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.FromArgb(70, 130, 180));
 0158            ws.Cells[1, col].Style.Font.Color.SetColor(System.Drawing.Color.White);
 159        }
 160
 0161        var row = 2;
 0162        foreach (var item in result.Items)
 163        {
 0164            ws.Cells[row, 1].Value = item.Index;
 0165            ws.Cells[row, 2].Value = item.PostingNumber;
 0166            ws.Cells[row, 3].Value = item.OfferId;
 0167            ws.Cells[row, 4].Value = item.ProductName;
 0168            ws.Cells[row, 5].Value = item.WarehouseName;
 0169            ws.Cells[row, 6].Value = item.StatusLabel;
 0170            ws.Cells[row, 7].Value = item.Quantity;
 0171            ws.Cells[row, 8].Value = item.CutoffDate?.ToString("dd.MM.yyyy HH:mm");
 0172            ws.Cells[row, 9].Value = item.SupplierName ?? "—";
 0173            ws.Cells[row, 10].Value = item.OrderedToSupplierDate?.ToString("dd.MM.yyyy HH:mm") ?? "—";
 0174            ws.Cells[row, 11].Value = item.OneCBalance.HasValue ? (object)item.OneCBalance.Value : "—";
 0175            ws.Cells[row, 12].Value = item.OneCReceiptDoc ?? "—";
 0176            ws.Cells[row, 13].Value = item.LabelUrl ?? item.LabelError ?? "—";
 0177            row++;
 178        }
 179
 0180        ws.Cells[ws.Dimension.Address].AutoFitColumns();
 181
 0182        var bytes = package.GetAsByteArray();
 0183        var fileName = $"assembly_list_{result.GeneratedAt:yyyyMMdd_HHmm}.xlsx";
 0184        return File(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName);
 0185    }
 186
 187    [HttpGet]
 188    public async Task<IActionResult> DownloadPdf(string jobId)
 189    {
 0190        var result = _cache.Get<AssemblyListResult>($"al_result_{jobId}");
 0191        if (result == null)
 0192            return NotFound("Данные не найдены или истёк срок хранения");
 193
 0194        QuestPDF.Settings.License = LicenseType.Community;
 195
 0196        var imageCache = new Dictionary<string, byte[]?>();
 0197        var httpClient = _httpClientFactory.CreateClient();
 0198        foreach (var item in result.Items.Where(i => !string.IsNullOrEmpty(i.ImageUrl)))
 199        {
 0200            var url = item.ImageUrl!;
 0201            if (!imageCache.ContainsKey(url))
 202            {
 0203                try { imageCache[url] = await httpClient.GetByteArrayAsync(url); }
 0204                catch { imageCache[url] = null; }
 205            }
 0206        }
 207
 0208        var pdfBytes = Document.Create(container =>
 0209        {
 0210            container.Page(page =>
 0211            {
 0212                page.Size(PageSizes.A4.Landscape());
 0213                page.Margin(15, Unit.Millimetre);
 0214                page.DefaultTextStyle(x => x.FontSize(8).FontFamily("Arial"));
 0215
 0216                page.Header().Column(col =>
 0217                {
 0218                    col.Item().Row(row =>
 0219                    {
 0220                        row.RelativeItem().Text("Лист подбора FBS").FontSize(14).Bold();
 0221                        row.ConstantItem(200).AlignRight()
 0222                            .Text($"Сформирован: {result.GeneratedAt:dd.MM.yyyy HH:mm}")
 0223                            .FontSize(8).FontColor("#9E9E9E");
 0224                    });
 0225                    col.Item().PaddingTop(4).LineHorizontal(0.5f).LineColor("#E0E0E0");
 0226                });
 0227
 0228                page.Content().PaddingTop(8).Table(table =>
 0229                {
 0230                    table.ColumnsDefinition(cols =>
 0231                    {
 0232                        cols.ConstantColumn(22);
 0233                        cols.ConstantColumn(35);
 0234                        cols.RelativeColumn(1.2f);
 0235                        cols.RelativeColumn(1.2f);
 0236                        cols.RelativeColumn(3);
 0237                        cols.RelativeColumn(1.5f);
 0238                        cols.RelativeColumn(1.2f);
 0239                        cols.ConstantColumn(30);
 0240                        cols.RelativeColumn(1);
 0241                    });
 0242
 0243                    table.Header(h =>
 0244                    {
 0245                        foreach (var hdr in new[] { "№", "Фото", "Отправление", "Артикул", "Название", "Склад", "Статус"
 0246                            h.Cell().Background("#1565C0").Padding(4).AlignCenter()
 0247                                .Text(hdr).FontColor("#FFFFFF").Bold().FontSize(7);
 0248                    });
 0249
 0250                    var rowIdx = 0;
 0251                    foreach (var item in result.Items)
 0252                    {
 0253                        rowIdx++;
 0254                        var bg = rowIdx % 2 == 0 ? "#F5F5F5" : "#FFFFFF";
 0255
 0256                        IContainer Cell(IContainer c) =>
 0257                            c.Background(bg).BorderBottom(0.3f).BorderColor("#E0E0E0").Padding(3).AlignMiddle();
 0258
 0259                        table.Cell().Element(Cell).Text(item.Index.ToString()).AlignCenter().FontSize(7);
 0260
 0261                        var imgCell = table.Cell().Element(Cell).AlignCenter();
 0262                        if (!string.IsNullOrEmpty(item.ImageUrl)
 0263                            && imageCache.TryGetValue(item.ImageUrl, out var imgBytes)
 0264                            && imgBytes != null)
 0265                            imgCell.Width(30).Height(30).Image(imgBytes).FitArea();
 0266                        else
 0267                            imgCell.Text("—").FontColor("#9E9E9E").FontSize(7);
 0268
 0269                        table.Cell().Element(Cell).Text(item.PostingNumber).FontSize(7);
 0270                        table.Cell().Element(Cell).Text(item.OfferId).FontSize(7);
 0271                        table.Cell().Element(Cell).Text(item.ProductName).FontSize(7);
 0272                        table.Cell().Element(Cell).Text(item.WarehouseName).FontSize(7);
 0273                        table.Cell().Element(Cell).Text(item.StatusLabel).FontSize(7);
 0274                        table.Cell().Element(Cell).Text(item.Quantity.ToString()).AlignCenter().Bold();
 0275                        table.Cell().Element(Cell).Text(item.CutoffDate?.ToString("dd.MM HH:mm") ?? "—").FontSize(7);
 0276                    }
 0277                });
 0278
 0279                page.Footer().AlignCenter().Text(t =>
 0280                {
 0281                    t.Span("Страница ").FontSize(7);
 0282                    t.CurrentPageNumber().FontSize(7);
 0283                    t.Span(" из ").FontSize(7);
 0284                    t.TotalPages().FontSize(7);
 0285                });
 0286            });
 0287        }).GeneratePdf();
 288
 0289        return File(pdfBytes, "application/pdf", $"assembly_list_{result.GeneratedAt:yyyyMMdd_HHmm}.pdf");
 0290    }
 291
 292    [HttpGet]
 293    public async Task<IActionResult> History()
 294    {
 0295        var runs = await _runRepository.GetRecentRunsAsync(100);
 0296        return View(runs);
 0297    }
 298
 299    [HttpGet]
 300    public async Task<IActionResult> HistoryDetail(long id)
 301    {
 0302        var run = await _runRepository.GetRunAsync(id);
 0303        if (run == null) return NotFound();
 0304        var items = await _runRepository.GetRunItemsAsync(id);
 0305        ViewBag.RunId = id;
 0306        ViewBag.Run = run;
 0307        return View(items);
 0308    }
 309
 310    [HttpGet]
 311    public async Task<IActionResult> DownloadHistoryExcel(long runId)
 312    {
 0313        var items = await _runRepository.GetRunItemsAsync(runId);
 0314        if (items.Count == 0)
 0315            return NotFound("Данные не найдены");
 316
 0317        ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
 0318        using var package = new ExcelPackage();
 0319        var ws = package.Workbook.Worksheets.Add("Лист подбора");
 320
 0321        var headers = new[] { "№", "Отправление", "Артикул", "Название", "Склад", "Статус", "Кол-во", "Дедлайн", "Постав
 0322        for (var col = 1; col <= headers.Length; col++)
 323        {
 0324            ws.Cells[1, col].Value = headers[col - 1];
 0325            ws.Cells[1, col].Style.Font.Bold = true;
 0326            ws.Cells[1, col].Style.Fill.PatternType = ExcelFillStyle.Solid;
 0327            ws.Cells[1, col].Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.FromArgb(70, 130, 180));
 0328            ws.Cells[1, col].Style.Font.Color.SetColor(System.Drawing.Color.White);
 329        }
 330
 0331        var row = 2;
 0332        foreach (var item in items)
 333        {
 0334            ws.Cells[row, 1].Value = item.ItemIndex;
 0335            ws.Cells[row, 2].Value = item.PostingNumber;
 0336            ws.Cells[row, 3].Value = item.OfferId;
 0337            ws.Cells[row, 4].Value = item.ProductName;
 0338            ws.Cells[row, 5].Value = item.WarehouseName;
 0339            ws.Cells[row, 6].Value = item.StatusLabel;
 0340            ws.Cells[row, 7].Value = item.Quantity;
 0341            ws.Cells[row, 8].Value = item.CutoffDate?.ToString("dd.MM.yyyy HH:mm");
 0342            ws.Cells[row, 9].Value = item.SupplierName ?? "—";
 0343            ws.Cells[row, 10].Value = item.OrderedToSupplierDate?.ToString("dd.MM.yyyy HH:mm") ?? "—";
 0344            ws.Cells[row, 11].Value = item.OneCBalance.HasValue ? (object)item.OneCBalance.Value : "—";
 0345            ws.Cells[row, 12].Value = item.OneCReceiptDoc ?? "—";
 0346            ws.Cells[row, 13].Value = item.LabelUrl ?? item.LabelError ?? "—";
 0347            row++;
 348        }
 0349        ws.Cells[ws.Dimension.Address].AutoFitColumns();
 350
 0351        return File(package.GetAsByteArray(),
 0352            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
 0353            $"assembly_list_run{runId}.xlsx");
 0354    }
 355
 356    [HttpGet]
 357    public async Task<IActionResult> DownloadHistoryPdf(long runId)
 358    {
 0359        var run = await _runRepository.GetRunAsync(runId);
 0360        var items = await _runRepository.GetRunItemsAsync(runId);
 0361        if (items.Count == 0)
 0362            return NotFound("Данные не найдены");
 363
 0364        QuestPDF.Settings.License = LicenseType.Community;
 365
 0366        var imageCache = new Dictionary<string, byte[]?>();
 0367        var httpClient = _httpClientFactory.CreateClient();
 0368        foreach (var item in items.Where(i => !string.IsNullOrEmpty(i.ImageUrl)))
 369        {
 0370            var url = item.ImageUrl!;
 0371            if (!imageCache.ContainsKey(url))
 372            {
 0373                try { imageCache[url] = await httpClient.GetByteArrayAsync(url); }
 0374                catch { imageCache[url] = null; }
 375            }
 0376        }
 377
 0378        var generatedAt = run?.StartedAtUtc.ToLocalTime() ?? DateTime.Now;
 0379        var pdfBytes = Document.Create(container =>
 0380        {
 0381            container.Page(page =>
 0382            {
 0383                page.Size(PageSizes.A4.Landscape());
 0384                page.Margin(15, Unit.Millimetre);
 0385                page.DefaultTextStyle(x => x.FontSize(8).FontFamily("Arial"));
 0386
 0387                page.Header().Column(col =>
 0388                {
 0389                    col.Item().Row(row =>
 0390                    {
 0391                        row.RelativeItem().Text("Лист подбора FBS").FontSize(14).Bold();
 0392                        row.ConstantItem(200).AlignRight()
 0393                            .Text($"Сформирован: {generatedAt:dd.MM.yyyy HH:mm}")
 0394                            .FontSize(8).FontColor("#9E9E9E");
 0395                    });
 0396                    col.Item().PaddingTop(4).LineHorizontal(0.5f).LineColor("#E0E0E0");
 0397                });
 0398
 0399                page.Content().PaddingTop(8).Table(table =>
 0400                {
 0401                    table.ColumnsDefinition(cols =>
 0402                    {
 0403                        cols.ConstantColumn(22);
 0404                        cols.ConstantColumn(35);
 0405                        cols.RelativeColumn(1.2f);
 0406                        cols.RelativeColumn(1.2f);
 0407                        cols.RelativeColumn(3);
 0408                        cols.RelativeColumn(1.5f);
 0409                        cols.RelativeColumn(1.2f);
 0410                        cols.ConstantColumn(30);
 0411                        cols.RelativeColumn(1);
 0412                    });
 0413
 0414                    table.Header(h =>
 0415                    {
 0416                        foreach (var hdr in new[] { "№", "Фото", "Отправление", "Артикул", "Название", "Склад", "Статус"
 0417                            h.Cell().Background("#1565C0").Padding(4).AlignCenter()
 0418                                .Text(hdr).FontColor("#FFFFFF").Bold().FontSize(7);
 0419                    });
 0420
 0421                    var rowIdx = 0;
 0422                    foreach (var item in items)
 0423                    {
 0424                        rowIdx++;
 0425                        var bg = rowIdx % 2 == 0 ? "#F5F5F5" : "#FFFFFF";
 0426
 0427                        IContainer Cell(IContainer c) =>
 0428                            c.Background(bg).BorderBottom(0.3f).BorderColor("#E0E0E0").Padding(3).AlignMiddle();
 0429
 0430                        table.Cell().Element(Cell).Text(item.ItemIndex.ToString()).AlignCenter().FontSize(7);
 0431
 0432                        var imgCell = table.Cell().Element(Cell).AlignCenter();
 0433                        if (!string.IsNullOrEmpty(item.ImageUrl)
 0434                            && imageCache.TryGetValue(item.ImageUrl, out var imgBytes)
 0435                            && imgBytes != null)
 0436                            imgCell.Width(30).Height(30).Image(imgBytes).FitArea();
 0437                        else
 0438                            imgCell.Text("—").FontColor("#9E9E9E").FontSize(7);
 0439
 0440                        table.Cell().Element(Cell).Text(item.PostingNumber).FontSize(7);
 0441                        table.Cell().Element(Cell).Text(item.OfferId).FontSize(7);
 0442                        table.Cell().Element(Cell).Text(item.ProductName).FontSize(7);
 0443                        table.Cell().Element(Cell).Text(item.WarehouseName).FontSize(7);
 0444                        table.Cell().Element(Cell).Text(item.StatusLabel).FontSize(7);
 0445                        table.Cell().Element(Cell).Text(item.Quantity.ToString()).AlignCenter().Bold();
 0446                        table.Cell().Element(Cell).Text(item.CutoffDate?.ToLocalTime().ToString("dd.MM HH:mm") ?? "—").F
 0447                    }
 0448                });
 0449
 0450                page.Footer().AlignCenter().Text(t =>
 0451                {
 0452                    t.Span("Страница ").FontSize(7);
 0453                    t.CurrentPageNumber().FontSize(7);
 0454                    t.Span(" из ").FontSize(7);
 0455                    t.TotalPages().FontSize(7);
 0456                });
 0457            });
 0458        }).GeneratePdf();
 459
 0460        return File(pdfBytes, "application/pdf", $"assembly_list_run{runId}.pdf");
 0461    }
 462
 463    [HttpGet]
 464    public async Task<IActionResult> LabelProxy(string url)
 465    {
 0466        if (string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out var uri))
 0467            return BadRequest();
 468
 0469        var host = uri.Host;
 0470        if (!host.EndsWith(".ozon.ru", StringComparison.OrdinalIgnoreCase)
 0471            && !host.EndsWith(".ozone.ru", StringComparison.OrdinalIgnoreCase)
 0472            && !host.EndsWith(".ozonusercontent.com", StringComparison.OrdinalIgnoreCase))
 0473            return BadRequest();
 474
 475        try
 476        {
 0477            var bytes = await _httpClientFactory.CreateClient().GetByteArrayAsync(url);
 0478            var fileName = Path.GetFileName(uri.LocalPath);
 0479            Response.Headers["Content-Disposition"] = $"attachment; filename=\"{fileName}\"";
 0480            return File(bytes, "application/pdf");
 481        }
 0482        catch
 483        {
 0484            return NotFound();
 485        }
 0486    }
 487
 488    public async Task<IActionResult> ImageProxy(string url)
 489    {
 0490        if (string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out var uri))
 0491            return BadRequest();
 492
 0493        var host = uri.Host;
 0494        if (!host.EndsWith(".ozon.ru", StringComparison.OrdinalIgnoreCase)
 0495            && !host.EndsWith(".ozone.ru", StringComparison.OrdinalIgnoreCase)
 0496            && !host.EndsWith(".ozonusercontent.com", StringComparison.OrdinalIgnoreCase))
 0497            return BadRequest();
 498
 499        try
 500        {
 0501            var bytes = await _httpClientFactory.CreateClient().GetByteArrayAsync(url);
 0502            return File(bytes, "image/jpeg");
 503        }
 0504        catch
 505        {
 0506            return NotFound();
 507        }
 0508    }
 509
 510    private static async Task WriteSSE(HttpResponse response, string eventName, string data)
 511    {
 0512        var sb = new StringBuilder();
 0513        sb.AppendLine($"event: {eventName}");
 0514        sb.AppendLine($"data: {data}");
 0515        sb.AppendLine();
 0516        await response.WriteAsync(sb.ToString());
 0517    }
 518}