TssRestClient .cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. using Quadarax.Application.TemporarySharedStorage.Client.Dtos.Request;
  2. using Quadarax.Application.TemporarySharedStorage.Client.Dtos.Response;
  3. using System.Net.Http.Json;
  4. using System.Text.Json;
  5. namespace Quadarax.Application.TemporarySharedStorage.Client
  6. {
  7. /// <summary>
  8. /// Low-level REST client for communicating with the QDR Temporary Shared Storage WordPress plugin API.
  9. /// Handles all HTTP communication, authentication, and error handling for the plugin's REST endpoints.
  10. ///
  11. /// This client supports:
  12. /// - Chunked file uploads (3-phase process: init → upload chunks → finalize)
  13. /// - Media management (get, update, delete, list)
  14. /// - Automatic API key authentication
  15. /// - Centralized error handling
  16. /// </summary>
  17. public class QdrTssRestClient : IDisposable
  18. {
  19. private readonly HttpClient _httpClient;
  20. private readonly string _baseUrl;
  21. private readonly string _apiKey;
  22. /// <summary>
  23. /// Initializes a new instance of the QdrTssRestClient.
  24. /// </summary>
  25. /// <param name="baseUrl">The base URL of the WordPress site (e.g., "https://example.com")</param>
  26. /// <param name="apiKey">The API key from the plugin settings for authentication</param>
  27. public QdrTssRestClient(string baseUrl, string apiKey)
  28. {
  29. _baseUrl = baseUrl.TrimEnd('/');
  30. _apiKey = apiKey;
  31. _httpClient = new HttpClient();
  32. // Add API key to all requests automatically
  33. _httpClient.DefaultRequestHeaders.Add("X-Api-Key", apiKey);
  34. // Set longer timeout for large file uploads
  35. _httpClient.Timeout = TimeSpan.FromMinutes(10);
  36. }
  37. /// <summary>
  38. /// Initializes a chunked file upload session.
  39. /// This is the first step in the 3-phase upload process.
  40. /// </summary>
  41. /// <param name="request">Upload initialization parameters (filename and file size)</param>
  42. /// <returns>Upload response containing the unique upload ID, or null if failed</returns>
  43. /// <remarks>
  44. /// The upload ID returned must be used in all subsequent chunk uploads and finalization.
  45. /// The server validates file size against configured limits during this step.
  46. /// </remarks>
  47. public async Task<InitUploadResponse?> InitializeUploadAsync(InitUploadRequest request)
  48. {
  49. var url = $"{_baseUrl}/wp-json/qdr-tss/v1/upload/init";
  50. var response = await _httpClient.PostAsJsonAsync(url, request);
  51. if (!response.IsSuccessStatusCode)
  52. {
  53. await HandleErrorResponse(response);
  54. return null;
  55. }
  56. return await response.Content.ReadFromJsonAsync<InitUploadResponse>();
  57. }
  58. /// <summary>
  59. /// Uploads a single chunk of file data.
  60. /// This is the second step in the 3-phase upload process, called once per chunk.
  61. /// </summary>
  62. /// <param name="uploadId">The upload ID from the initialization step</param>
  63. /// <param name="chunkIndex">The 0-based index of this chunk</param>
  64. /// <param name="totalChunks">Total number of chunks for this file</param>
  65. /// <param name="chunkData">The binary data for this chunk</param>
  66. /// <returns>Chunk upload response confirming receipt, or null if failed</returns>
  67. /// <remarks>
  68. /// Chunks must be uploaded sequentially (0, 1, 2, ...). The server stores chunks
  69. /// temporarily until all are received and the upload is finalized.
  70. /// </remarks>
  71. public async Task<ChunkUploadResponse?> UploadChunkAsync(
  72. string uploadId,
  73. int chunkIndex,
  74. int totalChunks,
  75. byte[] chunkData)
  76. {
  77. var url = $"{_baseUrl}/wp-json/qdr-tss/v1/upload/chunk";
  78. using var formData = new MultipartFormDataContent();
  79. formData.Add(new StringContent(uploadId), "upload_id");
  80. formData.Add(new StringContent(chunkIndex.ToString()), "chunk_index");
  81. formData.Add(new StringContent(totalChunks.ToString()), "total_chunks");
  82. var chunkContent = new ByteArrayContent(chunkData);
  83. formData.Add(chunkContent, "chunk", "chunk");
  84. var response = await _httpClient.PostAsync(url, formData);
  85. if (!response.IsSuccessStatusCode)
  86. {
  87. await HandleErrorResponse(response);
  88. return null;
  89. }
  90. return await response.Content.ReadFromJsonAsync<ChunkUploadResponse>();
  91. }
  92. /// <summary>
  93. /// Finalizes the chunked upload by combining all chunks and creating the shared file record.
  94. /// This is the third and final step in the upload process.
  95. /// </summary>
  96. /// <param name="request">Finalization parameters including file metadata and sharing settings</param>
  97. /// <returns>Final upload response with file ID and download links, or null if failed</returns>
  98. /// <remarks>
  99. /// This step combines all uploaded chunks into the final file, creates the database record
  100. /// with all sharing parameters (password, expiration, etc.), and returns the public access information.
  101. /// If this step fails, all uploaded chunks are automatically cleaned up.
  102. /// </remarks>
  103. public async Task<FinalizeUploadResponse?> FinalizeUploadAsync(FinalizeUploadRequest request)
  104. {
  105. var url = $"{_baseUrl}/wp-json/qdr-tss/v1/upload/finalize";
  106. var response = await _httpClient.PostAsJsonAsync(url, request);
  107. if (!response.IsSuccessStatusCode)
  108. {
  109. await HandleErrorResponse(response);
  110. return null;
  111. }
  112. return await response.Content.ReadFromJsonAsync<FinalizeUploadResponse>();
  113. }
  114. /// <summary>
  115. /// Retrieves detailed information about a specific shared file.
  116. /// </summary>
  117. /// <param name="fileId">The unique file ID returned from the upload process</param>
  118. /// <returns>Complete file information including metadata and statistics, or null if not found</returns>
  119. /// <remarks>
  120. /// The response includes download statistics, expiration status, and all file metadata.
  121. /// The password field is never included in responses for security reasons.
  122. /// </remarks>
  123. public async Task<MediaItemResponse?> GetMediaAsync(string fileId)
  124. {
  125. var url = $"{_baseUrl}/wp-json/qdr-tss/v1/media/{fileId}";
  126. var response = await _httpClient.GetAsync(url);
  127. if (!response.IsSuccessStatusCode)
  128. {
  129. await HandleErrorResponse(response);
  130. return null;
  131. }
  132. return await response.Content.ReadFromJsonAsync<MediaItemResponse>();
  133. }
  134. /// <summary>
  135. /// Updates editable properties of an existing shared file.
  136. /// </summary>
  137. /// <param name="fileId">The unique file ID to update</param>
  138. /// <param name="request">Update request with new property values (all properties are optional)</param>
  139. /// <returns>Updated file information, or null if failed</returns>
  140. /// <remarks>
  141. /// Only specific properties can be updated: message, active dates, reference, and description.
  142. /// The original file, password, and file ID cannot be changed after upload.
  143. /// Null properties in the request are ignored (not updated).
  144. /// </remarks>
  145. public async Task<MediaItemResponse?> UpdateMediaAsync(string fileId, UpdateMediaRequest request)
  146. {
  147. var url = $"{_baseUrl}/wp-json/qdr-tss/v1/media/{fileId}";
  148. var response = await _httpClient.PutAsJsonAsync(url, request);
  149. if (!response.IsSuccessStatusCode)
  150. {
  151. await HandleErrorResponse(response);
  152. return null;
  153. }
  154. return await response.Content.ReadFromJsonAsync<MediaItemResponse>();
  155. }
  156. /// <summary>
  157. /// Permanently deletes a shared file and all associated data.
  158. /// </summary>
  159. /// <param name="fileId">The unique file ID to delete</param>
  160. /// <returns>Deletion confirmation, or null if failed</returns>
  161. /// <remarks>
  162. /// This operation:
  163. /// - Removes the physical file from storage
  164. /// - Deletes the database record and all metadata
  165. /// - Makes the download link permanently inactive
  166. /// - Cannot be undone
  167. /// </remarks>
  168. public async Task<DeleteMediaResponse?> DeleteMediaAsync(string fileId)
  169. {
  170. var url = $"{_baseUrl}/wp-json/qdr-tss/v1/media/{fileId}";
  171. var response = await _httpClient.DeleteAsync(url);
  172. if (!response.IsSuccessStatusCode)
  173. {
  174. await HandleErrorResponse(response);
  175. return null;
  176. }
  177. return await response.Content.ReadFromJsonAsync<DeleteMediaResponse>();
  178. }
  179. /// <summary>
  180. /// Retrieves a paginated list of shared files with optional filtering and sorting.
  181. /// </summary>
  182. /// <param name="page">Page number (1-based, default: 1)</param>
  183. /// <param name="perPage">Items per page (default: 10, max typically 100)</param>
  184. /// <param name="orderBy">Field to sort by (default: "upload_date")</param>
  185. /// <param name="order">Sort direction: "ASC" or "DESC" (default: "DESC")</param>
  186. /// <param name="originalFileName">Filter by filename (partial match, optional)</param>
  187. /// <param name="reference">Filter by reference code (partial match, optional)</param>
  188. /// <returns>List of media items matching the criteria, or null if failed</returns>
  189. /// <remarks>
  190. /// Available sort fields: upload_date, original_file_name, file_size, download_count, active_to.
  191. /// Pagination headers (X-WP-Total, X-WP-TotalPages) are included in the HTTP response.
  192. /// Filters use case-insensitive partial matching (LIKE '%term%').
  193. /// </remarks>
  194. public async Task<List<MediaItemResponse>?> GetMediaListAsync(
  195. int page = 1,
  196. int perPage = 10,
  197. string orderBy = "upload_date",
  198. string order = "DESC",
  199. string? originalFileName = null,
  200. string? reference = null)
  201. {
  202. var queryParams = new List<string>
  203. {
  204. $"page={page}",
  205. $"per_page={perPage}",
  206. $"orderby={orderBy}",
  207. $"order={order}"
  208. };
  209. if (!string.IsNullOrEmpty(originalFileName))
  210. queryParams.Add($"original_file_name={Uri.EscapeDataString(originalFileName)}");
  211. if (!string.IsNullOrEmpty(reference))
  212. queryParams.Add($"reference={Uri.EscapeDataString(reference)}");
  213. var queryString = string.Join("&", queryParams);
  214. var url = $"{_baseUrl}/wp-json/qdr-tss/v1/media?{queryString}";
  215. var response = await _httpClient.GetAsync(url);
  216. if (!response.IsSuccessStatusCode)
  217. {
  218. await HandleErrorResponse(response);
  219. return null;
  220. }
  221. return await response.Content.ReadFromJsonAsync<List<MediaItemResponse>>();
  222. }
  223. /// <summary>
  224. /// Handles HTTP error responses by parsing and displaying error information.
  225. /// Attempts to parse WordPress-style error responses, falls back to raw HTTP error display.
  226. /// </summary>
  227. /// <param name="response">The failed HTTP response</param>
  228. private async Task HandleErrorResponse(HttpResponseMessage response)
  229. {
  230. var errorContent = await response.Content.ReadAsStringAsync();
  231. try
  232. {
  233. // Try to parse as WordPress API error response
  234. var errorResponse = JsonSerializer.Deserialize<ApiErrorResponse>(errorContent);
  235. Console.WriteLine($"API Error [{response.StatusCode}]: {errorResponse?.Message}");
  236. }
  237. catch
  238. {
  239. // Fall back to displaying raw error content
  240. Console.WriteLine($"HTTP Error [{response.StatusCode}]: {errorContent}");
  241. }
  242. }
  243. /// <summary>
  244. /// Disposes of the underlying HttpClient and releases resources.
  245. /// </summary>
  246. public void Dispose()
  247. {
  248. _httpClient?.Dispose();
  249. }
  250. }
  251. }