| | | 1 | | using Services.ApiServices.DropBoxApi; |
| | | 2 | | using OzonDomains.Models; |
| | | 3 | | using System.Text.RegularExpressions; |
| | | 4 | | using Services.SystemMonitoringServices; |
| | | 5 | | |
| | | 6 | | namespace Services.EmailRoutingServices; |
| | | 7 | | |
| | | 8 | | public class DropboxFileRouter : IDropboxFileRouter |
| | | 9 | | { |
| | | 10 | | private readonly DropboxApiClient _dropboxApiClient; |
| | | 11 | | private readonly ISystemMonitoringSignalWriter? _signals; |
| | | 12 | | |
| | 0 | 13 | | public DropboxFileRouter(DropboxApiClient dropboxApiClient, ISystemMonitoringSignalWriter? signals = null) |
| | | 14 | | { |
| | 0 | 15 | | _dropboxApiClient = dropboxApiClient; |
| | 0 | 16 | | _signals = signals; |
| | 0 | 17 | | } |
| | | 18 | | |
| | | 19 | | public async Task<DropboxRouteResult> RouteAsync( |
| | | 20 | | EmailRoutingRule rule, |
| | | 21 | | InboxMessageDto message, |
| | | 22 | | InboxAttachmentDto attachment, |
| | | 23 | | string hash) |
| | | 24 | | { |
| | | 25 | | try |
| | | 26 | | { |
| | 0 | 27 | | var folder = ResolveFolder(rule.DropboxFolderTemplate, message); |
| | 0 | 28 | | var fileName = await ResolveFileNameByPolicy(rule, folder, attachment.FileName, hash); |
| | | 29 | | |
| | 0 | 30 | | await _dropboxApiClient.UploadFileFromBytesAsync(attachment.FileBytes, folder, fileName); |
| | 0 | 31 | | var link = await _dropboxApiClient.GetDownloadLinkAsync(folder, fileName); |
| | 0 | 32 | | await (_signals?.RecordAsync("dropbox-routing", "success") ?? Task.CompletedTask); |
| | | 33 | | |
| | 0 | 34 | | return new DropboxRouteResult |
| | 0 | 35 | | { |
| | 0 | 36 | | IsSuccess = true, |
| | 0 | 37 | | FolderPath = folder, |
| | 0 | 38 | | FileName = fileName, |
| | 0 | 39 | | FilePath = $"{folder.TrimEnd('/')}/{fileName}", |
| | 0 | 40 | | DownloadLink = link |
| | 0 | 41 | | }; |
| | | 42 | | } |
| | | 43 | | catch (Exception ex) |
| | | 44 | | { |
| | 0 | 45 | | await (_signals?.RecordAsync("dropbox-routing", "failure", message: "Ошибка записи вложения в Dropbox") ?? T |
| | 0 | 46 | | return new DropboxRouteResult |
| | 0 | 47 | | { |
| | 0 | 48 | | IsSuccess = false, |
| | 0 | 49 | | Error = ex.Message |
| | 0 | 50 | | }; |
| | | 51 | | } |
| | 0 | 52 | | } |
| | | 53 | | |
| | | 54 | | private async Task<string> ResolveFileNameByPolicy( |
| | | 55 | | EmailRoutingRule rule, |
| | | 56 | | string folder, |
| | | 57 | | string fileName, |
| | | 58 | | string hash) |
| | | 59 | | { |
| | 0 | 60 | | switch (rule.ConflictPolicy) |
| | | 61 | | { |
| | | 62 | | case EmailRoutingConflictPolicy.Overwrite: |
| | 0 | 63 | | return fileName; |
| | | 64 | | |
| | | 65 | | case EmailRoutingConflictPolicy.SkipIfExists: |
| | | 66 | | { |
| | 0 | 67 | | var exists = await _dropboxApiClient.FileExistsAsync(folder, fileName); |
| | 0 | 68 | | return exists ? $"skipped_{hash[..8]}_{fileName}" : fileName; |
| | | 69 | | } |
| | | 70 | | |
| | | 71 | | case EmailRoutingConflictPolicy.VersionedName: |
| | | 72 | | { |
| | 0 | 73 | | var exists = await _dropboxApiClient.FileExistsAsync(folder, fileName); |
| | 0 | 74 | | if (!exists) |
| | | 75 | | { |
| | 0 | 76 | | return fileName; |
| | | 77 | | } |
| | | 78 | | |
| | 0 | 79 | | var ext = Path.GetExtension(fileName); |
| | 0 | 80 | | var name = Path.GetFileNameWithoutExtension(fileName); |
| | 0 | 81 | | return $"{name}_{DateTime.UtcNow:yyyyMMddHHmmss}{ext}"; |
| | | 82 | | } |
| | | 83 | | |
| | | 84 | | default: |
| | 0 | 85 | | return fileName; |
| | | 86 | | } |
| | 0 | 87 | | } |
| | | 88 | | |
| | | 89 | | private static string ResolveFolder(string template, InboxMessageDto message) |
| | | 90 | | { |
| | 0 | 91 | | var baseDate = message.ReceivedAtUtc?.ToUniversalTime() ?? DateTime.UtcNow; |
| | 0 | 92 | | var safeSender = SanitizePathToken(message.SenderEmail ?? "unknown"); |
| | 0 | 93 | | var safeSubject = SanitizePathToken(message.Subject ?? "no-subject"); |
| | 0 | 94 | | var folder = (template ?? string.Empty).Trim(); |
| | | 95 | | |
| | | 96 | | // Support {date:yyyy-MM-dd} style token from rule UI. |
| | 0 | 97 | | folder = Regex.Replace( |
| | 0 | 98 | | folder, |
| | 0 | 99 | | @"\{date:([^}]+)\}", |
| | 0 | 100 | | m => |
| | 0 | 101 | | { |
| | 0 | 102 | | var dateFormat = m.Groups[1].Value; |
| | 0 | 103 | | try |
| | 0 | 104 | | { |
| | 0 | 105 | | return baseDate.ToString(dateFormat); |
| | 0 | 106 | | } |
| | 0 | 107 | | catch |
| | 0 | 108 | | { |
| | 0 | 109 | | return baseDate.ToString("yyyy-MM-dd"); |
| | 0 | 110 | | } |
| | 0 | 111 | | }, |
| | 0 | 112 | | RegexOptions.CultureInvariant); |
| | | 113 | | |
| | 0 | 114 | | folder = folder |
| | 0 | 115 | | .Replace("{yyyy}", baseDate.ToString("yyyy")) |
| | 0 | 116 | | .Replace("{MM}", baseDate.ToString("MM")) |
| | 0 | 117 | | .Replace("{dd}", baseDate.ToString("dd")) |
| | 0 | 118 | | .Replace("{sender}", safeSender) |
| | 0 | 119 | | .Replace("{subject}", safeSubject); |
| | | 120 | | |
| | 0 | 121 | | folder = folder.Replace("\\", "/"); |
| | 0 | 122 | | while (folder.Contains("//")) |
| | | 123 | | { |
| | 0 | 124 | | folder = folder.Replace("//", "/"); |
| | | 125 | | } |
| | | 126 | | |
| | 0 | 127 | | if (!folder.StartsWith('/')) |
| | | 128 | | { |
| | 0 | 129 | | folder = "/" + folder; |
| | | 130 | | } |
| | | 131 | | |
| | 0 | 132 | | folder = folder.TrimEnd('/'); |
| | 0 | 133 | | return string.IsNullOrWhiteSpace(folder) ? "/" : folder; |
| | | 134 | | } |
| | | 135 | | |
| | | 136 | | private static string SanitizePathToken(string value) |
| | | 137 | | { |
| | 0 | 138 | | var invalid = Path.GetInvalidFileNameChars().Concat(new[] { '/', '\\' }).ToHashSet(); |
| | 0 | 139 | | return new string(value |
| | 0 | 140 | | .Trim() |
| | 0 | 141 | | .Select(ch => invalid.Contains(ch) ? '_' : ch) |
| | 0 | 142 | | .ToArray()); |
| | | 143 | | } |
| | | 144 | | } |