| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290 |
- using Quadarax.Application.TemporarySharedStorage.Client.Dtos.Request;
- using Quadarax.Application.TemporarySharedStorage.Client.Dtos.Response;
- using System.Net.Http.Json;
- using System.Text.Json;
- namespace Quadarax.Application.TemporarySharedStorage.Client
- {
- /// <summary>
- /// Low-level REST client for communicating with the QDR Temporary Shared Storage WordPress plugin API.
- /// Handles all HTTP communication, authentication, and error handling for the plugin's REST endpoints.
- ///
- /// This client supports:
- /// - Chunked file uploads (3-phase process: init → upload chunks → finalize)
- /// - Media management (get, update, delete, list)
- /// - Automatic API key authentication
- /// - Centralized error handling
- /// </summary>
- public class QdrTssRestClient : IDisposable
- {
- private readonly HttpClient _httpClient;
- private readonly string _baseUrl;
- private readonly string _apiKey;
-
- /// <summary>
- /// Initializes a new instance of the QdrTssRestClient.
- /// </summary>
- /// <param name="baseUrl">The base URL of the WordPress site (e.g., "https://example.com")</param>
- /// <param name="apiKey">The API key from the plugin settings for authentication</param>
- public QdrTssRestClient(string baseUrl, string apiKey)
- {
- _baseUrl = baseUrl.TrimEnd('/');
- _apiKey = apiKey;
-
- _httpClient = new HttpClient();
- // Add API key to all requests automatically
- _httpClient.DefaultRequestHeaders.Add("X-Api-Key", apiKey);
- // Set longer timeout for large file uploads
- _httpClient.Timeout = TimeSpan.FromMinutes(10);
- }
-
- /// <summary>
- /// Initializes a chunked file upload session.
- /// This is the first step in the 3-phase upload process.
- /// </summary>
- /// <param name="request">Upload initialization parameters (filename and file size)</param>
- /// <returns>Upload response containing the unique upload ID, or null if failed</returns>
- /// <remarks>
- /// The upload ID returned must be used in all subsequent chunk uploads and finalization.
- /// The server validates file size against configured limits during this step.
- /// </remarks>
- public async Task<InitUploadResponse?> InitializeUploadAsync(InitUploadRequest request)
- {
- var url = $"{_baseUrl}/wp-json/qdr-tss/v1/upload/init";
-
- var response = await _httpClient.PostAsJsonAsync(url, request);
-
- if (!response.IsSuccessStatusCode)
- {
- await HandleErrorResponse(response);
- return null;
- }
-
- return await response.Content.ReadFromJsonAsync<InitUploadResponse>();
- }
-
- /// <summary>
- /// Uploads a single chunk of file data.
- /// This is the second step in the 3-phase upload process, called once per chunk.
- /// </summary>
- /// <param name="uploadId">The upload ID from the initialization step</param>
- /// <param name="chunkIndex">The 0-based index of this chunk</param>
- /// <param name="totalChunks">Total number of chunks for this file</param>
- /// <param name="chunkData">The binary data for this chunk</param>
- /// <returns>Chunk upload response confirming receipt, or null if failed</returns>
- /// <remarks>
- /// Chunks must be uploaded sequentially (0, 1, 2, ...). The server stores chunks
- /// temporarily until all are received and the upload is finalized.
- /// </remarks>
- public async Task<ChunkUploadResponse?> UploadChunkAsync(
- string uploadId,
- int chunkIndex,
- int totalChunks,
- byte[] chunkData)
- {
- var url = $"{_baseUrl}/wp-json/qdr-tss/v1/upload/chunk";
-
- using var formData = new MultipartFormDataContent();
- formData.Add(new StringContent(uploadId), "upload_id");
- formData.Add(new StringContent(chunkIndex.ToString()), "chunk_index");
- formData.Add(new StringContent(totalChunks.ToString()), "total_chunks");
-
- var chunkContent = new ByteArrayContent(chunkData);
- formData.Add(chunkContent, "chunk", "chunk");
-
- var response = await _httpClient.PostAsync(url, formData);
-
- if (!response.IsSuccessStatusCode)
- {
- await HandleErrorResponse(response);
- return null;
- }
-
- return await response.Content.ReadFromJsonAsync<ChunkUploadResponse>();
- }
-
- /// <summary>
- /// Finalizes the chunked upload by combining all chunks and creating the shared file record.
- /// This is the third and final step in the upload process.
- /// </summary>
- /// <param name="request">Finalization parameters including file metadata and sharing settings</param>
- /// <returns>Final upload response with file ID and download links, or null if failed</returns>
- /// <remarks>
- /// This step combines all uploaded chunks into the final file, creates the database record
- /// with all sharing parameters (password, expiration, etc.), and returns the public access information.
- /// If this step fails, all uploaded chunks are automatically cleaned up.
- /// </remarks>
- public async Task<FinalizeUploadResponse?> FinalizeUploadAsync(FinalizeUploadRequest request)
- {
- var url = $"{_baseUrl}/wp-json/qdr-tss/v1/upload/finalize";
-
- var response = await _httpClient.PostAsJsonAsync(url, request);
-
- if (!response.IsSuccessStatusCode)
- {
- await HandleErrorResponse(response);
- return null;
- }
-
- return await response.Content.ReadFromJsonAsync<FinalizeUploadResponse>();
- }
-
- /// <summary>
- /// Retrieves detailed information about a specific shared file.
- /// </summary>
- /// <param name="fileId">The unique file ID returned from the upload process</param>
- /// <returns>Complete file information including metadata and statistics, or null if not found</returns>
- /// <remarks>
- /// The response includes download statistics, expiration status, and all file metadata.
- /// The password field is never included in responses for security reasons.
- /// </remarks>
- public async Task<MediaItemResponse?> GetMediaAsync(string fileId)
- {
- var url = $"{_baseUrl}/wp-json/qdr-tss/v1/media/{fileId}";
-
- var response = await _httpClient.GetAsync(url);
-
- if (!response.IsSuccessStatusCode)
- {
- await HandleErrorResponse(response);
- return null;
- }
-
- return await response.Content.ReadFromJsonAsync<MediaItemResponse>();
- }
-
- /// <summary>
- /// Updates editable properties of an existing shared file.
- /// </summary>
- /// <param name="fileId">The unique file ID to update</param>
- /// <param name="request">Update request with new property values (all properties are optional)</param>
- /// <returns>Updated file information, or null if failed</returns>
- /// <remarks>
- /// Only specific properties can be updated: message, active dates, reference, and description.
- /// The original file, password, and file ID cannot be changed after upload.
- /// Null properties in the request are ignored (not updated).
- /// </remarks>
- public async Task<MediaItemResponse?> UpdateMediaAsync(string fileId, UpdateMediaRequest request)
- {
- var url = $"{_baseUrl}/wp-json/qdr-tss/v1/media/{fileId}";
-
- var response = await _httpClient.PutAsJsonAsync(url, request);
-
- if (!response.IsSuccessStatusCode)
- {
- await HandleErrorResponse(response);
- return null;
- }
-
- return await response.Content.ReadFromJsonAsync<MediaItemResponse>();
- }
-
- /// <summary>
- /// Permanently deletes a shared file and all associated data.
- /// </summary>
- /// <param name="fileId">The unique file ID to delete</param>
- /// <returns>Deletion confirmation, or null if failed</returns>
- /// <remarks>
- /// This operation:
- /// - Removes the physical file from storage
- /// - Deletes the database record and all metadata
- /// - Makes the download link permanently inactive
- /// - Cannot be undone
- /// </remarks>
- public async Task<DeleteMediaResponse?> DeleteMediaAsync(string fileId)
- {
- var url = $"{_baseUrl}/wp-json/qdr-tss/v1/media/{fileId}";
-
- var response = await _httpClient.DeleteAsync(url);
-
- if (!response.IsSuccessStatusCode)
- {
- await HandleErrorResponse(response);
- return null;
- }
-
- return await response.Content.ReadFromJsonAsync<DeleteMediaResponse>();
- }
-
- /// <summary>
- /// Retrieves a paginated list of shared files with optional filtering and sorting.
- /// </summary>
- /// <param name="page">Page number (1-based, default: 1)</param>
- /// <param name="perPage">Items per page (default: 10, max typically 100)</param>
- /// <param name="orderBy">Field to sort by (default: "upload_date")</param>
- /// <param name="order">Sort direction: "ASC" or "DESC" (default: "DESC")</param>
- /// <param name="originalFileName">Filter by filename (partial match, optional)</param>
- /// <param name="reference">Filter by reference code (partial match, optional)</param>
- /// <returns>List of media items matching the criteria, or null if failed</returns>
- /// <remarks>
- /// Available sort fields: upload_date, original_file_name, file_size, download_count, active_to.
- /// Pagination headers (X-WP-Total, X-WP-TotalPages) are included in the HTTP response.
- /// Filters use case-insensitive partial matching (LIKE '%term%').
- /// </remarks>
- public async Task<List<MediaItemResponse>?> GetMediaListAsync(
- int page = 1,
- int perPage = 10,
- string orderBy = "upload_date",
- string order = "DESC",
- string? originalFileName = null,
- string? reference = null)
- {
- var queryParams = new List<string>
- {
- $"page={page}",
- $"per_page={perPage}",
- $"orderby={orderBy}",
- $"order={order}"
- };
-
- if (!string.IsNullOrEmpty(originalFileName))
- queryParams.Add($"original_file_name={Uri.EscapeDataString(originalFileName)}");
-
- if (!string.IsNullOrEmpty(reference))
- queryParams.Add($"reference={Uri.EscapeDataString(reference)}");
-
- var queryString = string.Join("&", queryParams);
- var url = $"{_baseUrl}/wp-json/qdr-tss/v1/media?{queryString}";
-
- var response = await _httpClient.GetAsync(url);
-
- if (!response.IsSuccessStatusCode)
- {
- await HandleErrorResponse(response);
- return null;
- }
-
- return await response.Content.ReadFromJsonAsync<List<MediaItemResponse>>();
- }
-
- /// <summary>
- /// Handles HTTP error responses by parsing and displaying error information.
- /// Attempts to parse WordPress-style error responses, falls back to raw HTTP error display.
- /// </summary>
- /// <param name="response">The failed HTTP response</param>
- private async Task HandleErrorResponse(HttpResponseMessage response)
- {
- var errorContent = await response.Content.ReadAsStringAsync();
-
- try
- {
- // Try to parse as WordPress API error response
- var errorResponse = JsonSerializer.Deserialize<ApiErrorResponse>(errorContent);
- Console.WriteLine($"API Error [{response.StatusCode}]: {errorResponse?.Message}");
- }
- catch
- {
- // Fall back to displaying raw error content
- Console.WriteLine($"HTTP Error [{response.StatusCode}]: {errorContent}");
- }
- }
-
- /// <summary>
- /// Disposes of the underlying HttpClient and releases resources.
- /// </summary>
- public void Dispose()
- {
- _httpClient?.Dispose();
- }
- }
- }
|