< Summary

Information
Class: Services.ApiServices.DropBoxApi.DropboxApiClient
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/DropBoxApi/DropboxApiClient.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 172
Coverable lines: 172
Total lines: 361
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 38
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/DropBoxApi/DropboxApiClient.cs

#LineLine coverage
 1using Dropbox.Api.Files;
 2using Dropbox.Api;
 3using Newtonsoft.Json;
 4using System.IO;
 5
 6
 7namespace Services.ApiServices.DropBoxApi
 8{
 9    public class DropboxApiClient
 10    {
 011        private static readonly HttpClient _tokenClient = new(new SocketsHttpHandler
 012        {
 013            PooledConnectionLifetime = TimeSpan.FromMinutes(5)
 014        });
 15
 16        private string _accessToken = null!;
 17        private readonly string _refreshToken;
 18        private readonly string _appKey;
 19        private readonly string _appSecret;
 20
 021        public DropboxApiClient(string refreshToken, string appKey, string appSecret)
 22        {
 023            _refreshToken = refreshToken;
 024            _appKey = appKey;
 025            _appSecret = appSecret;
 026        }
 27
 28        private async Task<string> RefreshAccessTokenAsync()
 29        {
 030            var client = _tokenClient;
 031            var requestContent = new FormUrlEncodedContent(new[]
 032            {
 033                new KeyValuePair<string, string>("grant_type", "refresh_token"),
 034                new KeyValuePair<string, string>("refresh_token", _refreshToken),
 035                new KeyValuePair<string, string>("client_id", _appKey),
 036                new KeyValuePair<string, string>("client_secret", _appSecret)
 037            });
 38
 039            var response = await client.PostAsync("https://api.dropbox.com/oauth2/token", requestContent);
 040            response.EnsureSuccessStatusCode();
 41
 042            var responseContent = await response.Content.ReadAsStringAsync();
 043            var tokenResponse = JsonConvert.DeserializeObject<Dictionary<string, string>>(responseContent);
 044            if (tokenResponse == null || !tokenResponse.TryGetValue("access_token", out var accessToken))
 045                throw new InvalidOperationException("Dropbox token response missing access_token");
 46
 047            _accessToken = accessToken;
 048            return _accessToken;
 049        }
 50
 51        public async Task<string> GetViewLinkAsync(string filePath, string fileName)
 52        {
 053            await Task.CompletedTask;
 054            var baseSegments = new[] { "Приложения", "Studio2", "Orders" }
 055                .Select(Uri.EscapeDataString);
 056            var pathSegments = filePath.Split('/', StringSplitOptions.RemoveEmptyEntries)
 057                .Select(Uri.EscapeDataString);
 058            var fileSegment = Uri.EscapeDataString(fileName);
 059            var allSegments = baseSegments.Concat(pathSegments).Concat(new[] { fileSegment });
 060            var encodedPath = string.Join('/', allSegments);
 061            return $"https://www.dropbox.com/home/{encodedPath}";
 062        }
 63
 64        public async Task UploadFileAsync(string localFilePath, string dropboxFolderPath, string dropboxFileName)
 65        {
 066            await RefreshAccessTokenAsync();
 67
 068            using (var dropboxClient = new DropboxClient(_accessToken))
 69            {
 070                using (var fileStream = new FileStream(localFilePath, FileMode.Open, FileAccess.Read))
 71                {
 072                    await dropboxClient.Files.UploadAsync(
 073                        path: $"{dropboxFolderPath}/{dropboxFileName}",
 074                        WriteMode.Overwrite.Instance,
 075                        body: fileStream
 076                    );
 077                }
 078            }
 079        }
 80
 81        /// <summary>
 82        /// Получение ссылки для скачивания файла из Dropbox
 83        /// </summary>
 84        public async Task<string> GetDownloadLinkAsync(string dropboxFolderPath, string fileName)
 85        {
 086            await RefreshAccessTokenAsync();
 87
 088            using (var dropboxClient = new DropboxClient(_accessToken))
 89            {
 090                var fullPath = $"{dropboxFolderPath.TrimEnd('/')}/{fileName}";
 091                var tempLinkResult = await dropboxClient.Files.GetTemporaryLinkAsync(fullPath);
 092                return tempLinkResult.Link;
 93            }
 094        }
 95
 96        /// <summary>
 97        /// Возвращает файлы из подпапок указанной папки Dropbox вместе с временными ссылками.
 98        /// </summary>
 99        /// <param name="folderPath">Путь к папке с подпапками.</param>
 100        /// <param name="maxFilesPerFolder">
 101        /// Максимум файлов на подпапку (по дате изменения, новые сначала). null — без ограничения (все файлы).
 102        /// </param>
 103        /// <param name="filterKnownFileNames">
 104        /// Необязательный колбэк: получает список имён файлов подпапки, возвращает набор уже известных —
 105        /// для них временная ссылка не запрашивается (экономит дорогие вызовы Dropbox API).
 106        /// </param>
 107        public async Task<Dictionary<string, Dictionary<string, string>>> GetFolderContentsAsync(
 108            string folderPath,
 109            int? maxFilesPerFolder = 10,
 110            Func<IReadOnlyCollection<string>, Task<HashSet<string>>>? filterKnownFileNames = null)
 111        {
 0112            var result = new Dictionary<string, Dictionary<string, string>>();
 113            try
 114            {
 0115                await RefreshAccessTokenAsync();
 0116                using (var client = new DropboxClient(_accessToken))
 117                {
 118                    // Получаем список файлов и папок в указанной папке
 0119                    var list = await client.Files.ListFolderAsync(folderPath);
 120
 121                    // Проходим по всем элементам в папке
 0122                    foreach (var item in list.Entries.Where(i => i.IsFolder))
 123                    {
 0124                        var folderName = item.AsFolder.Name;
 0125                        var folderPathLower = item.AsFolder.PathLower;
 126
 127                        // Получаем список файлов в подпапке (с пагинацией, если лимит не задан —
 128                        // иначе часть файлов Dropbox не покажет за один вызов ListFolderAsync)
 0129                        var folderFiles = await client.Files.ListFolderAsync(folderPathLower);
 0130                        var entries = folderFiles.Entries.ToList();
 0131                        if (maxFilesPerFolder == null)
 132                        {
 0133                            var hasMore = folderFiles.HasMore;
 0134                            var cursor = folderFiles.Cursor;
 0135                            while (hasMore)
 136                            {
 0137                                var next = await client.Files.ListFolderContinueAsync(cursor);
 0138                                entries.AddRange(next.Entries);
 0139                                hasMore = next.HasMore;
 0140                                cursor = next.Cursor;
 141                            }
 142                        }
 143
 144                        // Сортируем файлы по дате изменения (от новых к старым), при заданном лимите — берём верхушку
 0145                        var candidateFiles = entries
 0146                            .Where(i => i.IsFile)
 0147                            .OrderByDescending(f => f.AsFile.ClientModified) // Сначала новые файлы
 0148                            .ToList();
 0149                        if (maxFilesPerFolder.HasValue)
 150                        {
 0151                            candidateFiles = candidateFiles.Take(maxFilesPerFolder.Value).ToList();
 152                        }
 153
 154                        // Отсеиваем уже известные файлы ДО генерации временных ссылок
 0155                        var known = filterKnownFileNames == null
 0156                            ? new HashSet<string>()
 0157                            : await filterKnownFileNames(candidateFiles.Select(f => f.AsFile.Name).ToList());
 158
 159                        // Создаем словарь для хранения названий файлов и ссылок
 0160                        var fileLinks = new Dictionary<string, string>();
 0161                        foreach (var file in candidateFiles)
 162                        {
 0163                            var fileName = file.AsFile.Name;
 0164                            if (known.Contains(fileName))
 165                                continue;
 166
 0167                            var filePath = file.AsFile.PathLower;
 0168                            var link = await client.Files.GetTemporaryLinkAsync(filePath);
 0169                            fileLinks[fileName] = link.Link;
 0170                        }
 171
 172                        // Добавляем в результат
 0173                        result[folderName] = fileLinks;
 0174                    }
 0175                }
 0176            }
 177            catch (Exception e)
 178            {
 0179                Console.WriteLine(e);
 0180            }
 181
 0182            return result;
 0183        }
 184
 185        /// <summary>
 186        /// Проверяет существование файла в Dropbox
 187        /// </summary>
 188        public async Task<bool> FileExistsAsync(string dropboxFolderPath, string fileName)
 189        {
 190            try
 191            {
 0192                await RefreshAccessTokenAsync();
 0193                using (var client = new DropboxClient(_accessToken))
 194                {
 0195                    var fullPath = $"{dropboxFolderPath.TrimEnd('/')}/{fileName}";
 196                    try
 197                    {
 0198                        var metadata = await client.Files.GetMetadataAsync(fullPath);
 0199                        return metadata != null && metadata.IsFile;
 200                    }
 0201                    catch (Exception ex)
 202                    {
 203                        // Проверяем, является ли это ошибкой "файл не найден"
 204                        // В Dropbox API это обычно HttpException с кодом 409 или 404
 0205                        if (ex.Message.Contains("not_found") ||
 0206                            ex.Message.Contains("path") ||
 0207                            ex.Message.Contains("not found") ||
 0208                            ex.GetType().Name.Contains("HttpException"))
 209                        {
 0210                            return false;
 211                        }
 212
 213                        // Другие ошибки - считаем что файла нет
 214                        return false;
 215                    }
 216                }
 217            }
 0218            catch
 219            {
 0220                return false;
 221            }
 0222        }
 223
 224        /// <summary>
 225        /// Загружает файл из байтов в Dropbox
 226        /// </summary>
 227        public async Task UploadFileFromBytesAsync(byte[] fileBytes, string dropboxFolderPath, string dropboxFileName)
 228        {
 0229            await RefreshAccessTokenAsync();
 230
 0231            using (var dropboxClient = new DropboxClient(_accessToken))
 232            {
 0233                using (var memoryStream = new MemoryStream(fileBytes))
 234                {
 0235                    var fullPath = $"{dropboxFolderPath.TrimEnd('/')}/{dropboxFileName}";
 0236                    await dropboxClient.Files.UploadAsync(
 0237                        path: fullPath,
 0238                        WriteMode.Overwrite.Instance,
 0239                        body: memoryStream
 0240                    );
 0241                }
 0242            }
 0243        }
 244
 245        /// <summary>
 246        /// Загружает файл из байтов и возвращает ссылку для скачивания
 247        /// </summary>
 248        public async Task<string> UploadFileAndGetLinkAsync(byte[] fileBytes, string dropboxFolderPath,
 249            string dropboxFileName)
 250        {
 0251            await UploadFileFromBytesAsync(fileBytes, dropboxFolderPath, dropboxFileName);
 0252            return await GetDownloadLinkAsync(dropboxFolderPath, dropboxFileName);
 0253        }
 254
 255        /// <summary>
 256        /// Получает список всех файлов в папке с их метаданными
 257        /// </summary>
 258        public async Task<List<DropboxFileInfo>> GetFilesListAsync(string dropboxFolderPath)
 259        {
 0260            var filesList = new List<DropboxFileInfo>();
 261            try
 262            {
 0263                await RefreshAccessTokenAsync();
 0264                using (var client = new DropboxClient(_accessToken))
 265                {
 0266                    var list = await client.Files.ListFolderAsync(dropboxFolderPath);
 267
 0268                    foreach (var item in list.Entries.Where(i => i.IsFile))
 269                    {
 0270                        var file = item.AsFile;
 0271                        filesList.Add(new DropboxFileInfo
 0272                        {
 0273                            Name = file.Name,
 0274                            Path = file.PathLower,
 0275                            Size = file.Size,
 0276                            Modified = file.ServerModified
 0277                        });
 278                    }
 0279                }
 0280            }
 0281            catch (Exception e)
 282            {
 0283                Console.WriteLine($"Ошибка при получении списка файлов: {e.Message}");
 0284            }
 285
 0286            return filesList.OrderByDescending(f => f.Modified).ToList();
 0287        }
 288
 289        /// <summary>
 290        /// Индекс имён PDF в папке (без расширения) — один list_folder вместо N get_metadata.
 291        /// </summary>
 292        public async Task<HashSet<string>> GetPdfShipmentNumberIndexAsync(string dropboxFolderPath)
 293        {
 0294            var index = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 295            try
 296            {
 0297                await RefreshAccessTokenAsync();
 0298                using var client = new DropboxClient(_accessToken);
 0299                var list = await client.Files.ListFolderAsync(dropboxFolderPath);
 0300                AddPdfNamesToIndex(list, index);
 0301                while (list.HasMore)
 302                {
 0303                    list = await client.Files.ListFolderContinueAsync(list.Cursor);
 0304                    AddPdfNamesToIndex(list, index);
 305                }
 0306            }
 0307            catch (Exception e)
 308            {
 0309                Console.WriteLine($"Ошибка при построении индекса этикеток: {e.Message}");
 0310            }
 311
 0312            return index;
 0313        }
 314
 315        private static void AddPdfNamesToIndex(ListFolderResult list, HashSet<string> index)
 316        {
 0317            foreach (var item in list.Entries.Where(i => i.IsFile))
 318            {
 0319                var name = item.AsFile.Name;
 0320                if (!name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
 321                {
 322                    continue;
 323                }
 324
 0325                index.Add(name[..^4]);
 326            }
 0327        }
 328
 329        /// <summary>
 330        /// Получает файл из Dropbox в виде байтов
 331        /// </summary>
 332        public async Task<byte[]> DownloadFileAsync(string dropboxFolderPath, string fileName)
 333        {
 0334            await RefreshAccessTokenAsync();
 335
 0336            using (var dropboxClient = new DropboxClient(_accessToken))
 337            {
 0338                var fullPath = $"{dropboxFolderPath.TrimEnd('/')}/{fileName}";
 0339                var downloadResult = await dropboxClient.Files.DownloadAsync(fullPath);
 340
 0341                using (var contentStream = await downloadResult.GetContentAsStreamAsync())
 0342                using (var memoryStream = new MemoryStream())
 343                {
 0344                    await contentStream.CopyToAsync(memoryStream);
 0345                    return memoryStream.ToArray();
 346                }
 347            }
 0348        }
 349    }
 350
 351    /// <summary>
 352    /// Информация о файле в Dropbox
 353    /// </summary>
 354    public class DropboxFileInfo
 355    {
 356        public string Name { get; set; } = null!;
 357        public string Path { get; set; } = null!;
 358        public ulong Size { get; set; }
 359        public DateTime Modified { get; set; }
 360    }
 361}