|
@@ -0,0 +1,290 @@
|
|
|
|
|
+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();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|