Explorar o código

qdr-temporary-shared-storage: add initial .net 9 client library, fix pack.cmd

Dalibor Votruba hai 1 ano
pai
achega
ef4fed917e
Modificáronse 18 ficheiros con 1019 adicións e 10 borrados
  1. 38 0
      .gitignore
  2. 2 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client.console/Program.cs
  3. 10 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client.console/qdr.app.tss.client.console.csproj
  4. 31 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client.sln
  5. 70 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Request/FinalizeUploadRequest.cs
  6. 23 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Request/InitUploadRequest.cs
  7. 41 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Request/UpdateMediaRequest.cs
  8. 50 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Request/UploadFileRequest.cs
  9. 29 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Response/ApiErrorResponse.cs
  10. 29 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Response/ChunkUploadResponse.cs
  11. 22 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Response/DeleteMediaResponse.cs
  12. 34 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Response/FinalizeUploadResponse.cs
  13. 24 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Response/InitUploadResponse.cs
  14. 83 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Response/MediaItemResponse.cs
  15. 222 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Services/FileUploadService.cs
  16. 290 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/TssRestClient .cs
  17. 14 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/qdr.app.tss.client.csproj
  18. 7 10
      qdr-temporary-shared-storage/pack.cmd

+ 38 - 0
.gitignore

@@ -0,0 +1,38 @@
+
+#ignore thumbnails created by windows -
+Thumbs.db
+#Ignore files build by Visual Studio
+*.obj
+*.pdb
+*.user
+*.aps
+*.pch
+*.vspscc
+*_i.c
+*_p.c
+*.ncb
+*.suo
+*.tlb
+*.tlh
+*.bak
+*.cache
+*.ilk
+*.log
+*.suo
+[Bb]in
+[Dd]ebug*/
+*.lib
+*.sbr
+obj/
+[Rr]elease*/
+_ReSharper*/
+[Tt]est[Rr]esult*
+.idea/
+.vs/
+DTAR_*/
+packages/
+*.vssscc
+*.7z
+*.xsb
+*.pjb
+*.nupkg

+ 2 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client.console/Program.cs

@@ -0,0 +1,2 @@
+// See https://aka.ms/new-console-template for more information
+Console.WriteLine("Hello, World!");

+ 10 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client.console/qdr.app.tss.client.console.csproj

@@ -0,0 +1,10 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net9.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+</Project>

+ 31 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client.sln

@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.14.36109.1 d17.14
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "qdr.app.tss.client.console", "qdr.app.tss.client.console\qdr.app.tss.client.console.csproj", "{76FEF746-99A6-4ACD-AFDF-B87B9FE1560B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "qdr.app.tss.client", "qdr.app.tss.client\qdr.app.tss.client.csproj", "{EAE16F98-4575-40E5-B0E7-8D0D66219B65}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{76FEF746-99A6-4ACD-AFDF-B87B9FE1560B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{76FEF746-99A6-4ACD-AFDF-B87B9FE1560B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{76FEF746-99A6-4ACD-AFDF-B87B9FE1560B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{76FEF746-99A6-4ACD-AFDF-B87B9FE1560B}.Release|Any CPU.Build.0 = Release|Any CPU
+		{EAE16F98-4575-40E5-B0E7-8D0D66219B65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{EAE16F98-4575-40E5-B0E7-8D0D66219B65}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{EAE16F98-4575-40E5-B0E7-8D0D66219B65}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{EAE16F98-4575-40E5-B0E7-8D0D66219B65}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {FEFAA0AE-8256-4045-BAC0-21D274F9EEEE}
+	EndGlobalSection
+EndGlobal

+ 70 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Request/FinalizeUploadRequest.cs

@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+
+namespace Quadarax.Application.TemporarySharedStorage.Client.Dtos.Request
+{
+   /// <summary>
+    /// Request DTO for finalizing the chunked upload.
+    /// This combines all chunks and creates the final shared file record.
+    /// </summary>
+    public class FinalizeUploadRequest
+    {
+        /// <summary>
+        /// The upload ID from the initialization step
+        /// </summary>
+        [JsonPropertyName("upload_id")]
+        public string UploadId { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Total number of chunks that were uploaded
+        /// </summary>
+        [JsonPropertyName("total_chunks")]
+        public int TotalChunks { get; set; }
+        
+        /// <summary>
+        /// Original filename that will be preserved for download
+        /// </summary>
+        [JsonPropertyName("original_file_name")]
+        public string OriginalFileName { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Human-readable description of the file (displayed to downloaders)
+        /// </summary>
+        [JsonPropertyName("description")]
+        public string Description { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Optional custom message for downloaders (e.g., instructions or notes)
+        /// </summary>
+        [JsonPropertyName("message")]
+        public string Message { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Password required for downloading the file
+        /// </summary>
+        [JsonPropertyName("password")]
+        public string Password { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Optional reference code for additional verification during download
+        /// </summary>
+        [JsonPropertyName("reference")]
+        public string Reference { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Date and time when the file becomes available for download (format: yyyy-MM-dd HH:mm:ss)
+        /// </summary>
+        [JsonPropertyName("active_from")]
+        public string ActiveFrom { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Date and time when the file expires and is no longer available (format: yyyy-MM-dd HH:mm:ss)
+        /// </summary>
+        [JsonPropertyName("active_to")]
+        public string ActiveTo { get; set; } = string.Empty;
+    }
+}

+ 23 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Request/InitUploadRequest.cs

@@ -0,0 +1,23 @@
+using System.Text.Json.Serialization;
+
+namespace Quadarax.Application.TemporarySharedStorage.Client.Dtos.Request
+{
+    /// <summary>
+    /// Request DTO for initializing a chunked file upload.
+    /// This is the first step in the 3-phase upload process.
+    /// </summary>
+    public class InitUploadRequest
+    {
+        /// <summary>
+        /// The original name of the file being uploaded (e.g., "document.pdf")
+        /// </summary>
+        [JsonPropertyName("original_file_name")]
+        public string OriginalFileName { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// The total size of the file in bytes
+        /// </summary>
+        [JsonPropertyName("file_size")]
+        public long FileSize { get; set; }
+    }
+}

+ 41 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Request/UpdateMediaRequest.cs

@@ -0,0 +1,41 @@
+using System.Text.Json.Serialization;
+
+namespace Quadarax.Application.TemporarySharedStorage.Client.Dtos.Request
+{
+     /// <summary>
+    /// Request DTO for updating properties of an existing media item.
+    /// All properties are optional - only provided properties will be updated.
+    /// </summary>
+    public class UpdateMediaRequest
+    {
+        /// <summary>
+        /// Update the custom message for downloaders
+        /// </summary>
+        [JsonPropertyName("message")]
+        public string? Message { get; set; }
+        
+        /// <summary>
+        /// Update when the file becomes available (format: yyyy-MM-dd HH:mm:ss)
+        /// </summary>
+        [JsonPropertyName("active_from")]
+        public string? ActiveFrom { get; set; }
+        
+        /// <summary>
+        /// Update when the file expires (format: yyyy-MM-dd HH:mm:ss)
+        /// </summary>
+        [JsonPropertyName("active_to")]
+        public string? ActiveTo { get; set; }
+        
+        /// <summary>
+        /// Update the reference code for download verification
+        /// </summary>
+        [JsonPropertyName("reference")]
+        public string? Reference { get; set; }
+        
+        /// <summary>
+        /// Update the file description
+        /// </summary>
+        [JsonPropertyName("description")]
+        public string? Description { get; set; }
+    }
+}

+ 50 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Request/UploadFileRequest.cs

@@ -0,0 +1,50 @@
+namespace Quadarax.Application.TemporarySharedStorage.Client.Dtos.Request
+{
+     /// <summary>
+    /// High-level request model for the file upload service.
+    /// Used by the service layer to encapsulate all upload parameters.
+    /// </summary>
+    public class UploadFileRequest
+    {
+        /// <summary>
+        /// Full path to the local file to be uploaded
+        /// </summary>
+        public string FilePath { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Description that will be shown to downloaders
+        /// </summary>
+        public string Description { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Password required for downloading the file
+        /// </summary>
+        public string Password { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Optional message or instructions for downloaders
+        /// </summary>
+        public string Message { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Optional reference code for additional download verification
+        /// </summary>
+        public string Reference { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// When the file should become available for download. If null, defaults to current time.
+        /// </summary>
+        public DateTime? ActiveFrom { get; set; }
+        
+        /// <summary>
+        /// When the file should expire. If null, defaults to 30 days from upload.
+        /// </summary>
+        public DateTime? ActiveTo { get; set; }
+        
+        /// <summary>
+        /// Size of chunks for uploading in bytes. Default is 1MB (1048576 bytes).
+        /// Larger chunks = fewer HTTP requests but more memory usage.
+        /// </summary>
+        public int ChunkSize { get; set; } = 1048576; // Default 1MB
+    }
+}

+ 29 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Response/ApiErrorResponse.cs

@@ -0,0 +1,29 @@
+using System.Text.Json.Serialization;
+
+namespace Quadarax.Application.TemporarySharedStorage.Client.Dtos.Response
+{
+    /// <summary>
+    /// Response DTO for API errors.
+    /// Used to parse error responses from the WordPress plugin.
+    /// </summary>
+    public class ApiErrorResponse
+    {
+        /// <summary>
+        /// Error code identifier (e.g., "qdr_tss_not_found")
+        /// </summary>
+        [JsonPropertyName("code")]
+        public string Code { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Human-readable error message
+        /// </summary>
+        [JsonPropertyName("message")]
+        public string Message { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Additional error data (e.g., HTTP status code)
+        /// </summary>
+        [JsonPropertyName("data")]
+        public object? Data { get; set; }
+    }
+}

+ 29 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Response/ChunkUploadResponse.cs

@@ -0,0 +1,29 @@
+using System.Text.Json.Serialization;
+
+namespace Quadarax.Application.TemporarySharedStorage.Client.Dtos.Response
+{
+   /// <summary>
+    /// Response DTO from uploading an individual file chunk.
+    /// Confirms that the chunk was received and stored successfully.
+    /// </summary>
+    public class ChunkUploadResponse
+    {
+        /// <summary>
+        /// The upload ID this chunk belongs to
+        /// </summary>
+        [JsonPropertyName("upload_id")]
+        public string UploadId { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// The index of the chunk that was uploaded (0-based)
+        /// </summary>
+        [JsonPropertyName("chunk_index")]
+        public int ChunkIndex { get; set; }
+        
+        /// <summary>
+        /// Status of the chunk upload. Should be "chunk_uploaded" on success.
+        /// </summary>
+        [JsonPropertyName("status")]
+        public string Status { get; set; } = string.Empty;
+    }
+}

+ 22 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Response/DeleteMediaResponse.cs

@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+
+namespace Quadarax.Application.TemporarySharedStorage.Client.Dtos.Response
+{
+     /// <summary>
+    /// Response DTO confirming successful deletion of a media item.
+    /// </summary>
+    public class DeleteMediaResponse
+    {
+        /// <summary>
+        /// Confirms whether the file was successfully deleted
+        /// </summary>
+        [JsonPropertyName("deleted")]
+        public bool Deleted { get; set; }
+        
+        /// <summary>
+        /// The file ID that was deleted
+        /// </summary>
+        [JsonPropertyName("fileid")]
+        public string FileId { get; set; } = string.Empty;
+    }
+}

+ 34 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Response/FinalizeUploadResponse.cs

@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+
+namespace Quadarax.Application.TemporarySharedStorage.Client.Dtos.Response
+{
+    /// <summary>
+    /// Response DTO from finalizing the upload.
+    /// Contains the file ID and download information for the successfully uploaded file.
+    /// </summary>
+    public class FinalizeUploadResponse
+    {
+        /// <summary>
+        /// Unique file identifier generated by the system
+        /// </summary>
+        [JsonPropertyName("fileid")]
+        public string FileId { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Direct URL to the download page for this file
+        /// </summary>
+        [JsonPropertyName("permalink")]
+        public string Permalink { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// WordPress shortcode that can be embedded in posts/pages to display download form
+        /// </summary>
+        [JsonPropertyName("shortcode")]
+        public string Shortcode { get; set; } = string.Empty;
+    }
+}

+ 24 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Response/InitUploadResponse.cs

@@ -0,0 +1,24 @@
+using System.Text.Json.Serialization;
+
+namespace Quadarax.Application.TemporarySharedStorage.Client.Dtos.Response
+{
+    /// <summary>
+    /// Response DTO from the upload initialization endpoint.
+    /// Contains the unique upload ID needed for subsequent chunk uploads.
+    /// </summary>
+    public class InitUploadResponse
+    {
+        /// <summary>
+        /// Unique identifier for this upload session. Used in all subsequent chunk uploads.
+        /// </summary>
+        [JsonPropertyName("upload_id")]
+        public string UploadId { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Status of the initialization. Should be "initialized" on success.
+        /// </summary>
+        [JsonPropertyName("status")]
+        public string Status { get; set; } = string.Empty;
+    }
+
+}

+ 83 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Dtos/Response/MediaItemResponse.cs

@@ -0,0 +1,83 @@
+using System.Text.Json.Serialization;
+
+namespace Quadarax.Application.TemporarySharedStorage.Client.Dtos.Response
+{
+    /// <summary>
+    /// Response DTO representing a complete media item with all its properties.
+    /// Used when retrieving file details or lists of files.
+    /// </summary>
+    public class MediaItemResponse
+    {
+        /// <summary>
+        /// Internal database ID (auto-increment)
+        /// </summary>
+        [JsonPropertyName("id")]
+        public int Id { get; set; }
+        
+        /// <summary>
+        /// Unique file identifier used for public access
+        /// </summary>
+        [JsonPropertyName("fileid")]
+        public string FileId { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Original filename as uploaded
+        /// </summary>
+        [JsonPropertyName("original_file_name")]
+        public string OriginalFileName { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// User-provided description of the file
+        /// </summary>
+        [JsonPropertyName("description")]
+        public string Description { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Optional custom message for downloaders
+        /// </summary>
+        [JsonPropertyName("message")]
+        public string Message { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Date/time when file becomes available (format: yyyy-MM-dd HH:mm:ss)
+        /// </summary>
+        [JsonPropertyName("active_from")]
+        public string ActiveFrom { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Date/time when file expires (format: yyyy-MM-dd HH:mm:ss)
+        /// </summary>
+        [JsonPropertyName("active_to")]
+        public string ActiveTo { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Optional reference code for download verification
+        /// </summary>
+        [JsonPropertyName("reference")]
+        public string Reference { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// When the file was originally uploaded (format: yyyy-MM-dd HH:mm:ss)
+        /// </summary>
+        [JsonPropertyName("upload_date")]
+        public string UploadDate { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Number of times this file has been downloaded
+        /// </summary>
+        [JsonPropertyName("download_count")]
+        public int DownloadCount { get; set; }
+        
+        /// <summary>
+        /// Date/time of the most recent download, null if never downloaded
+        /// </summary>
+        [JsonPropertyName("last_download")]
+        public string? LastDownload { get; set; }
+        
+        /// <summary>
+        /// File size in bytes
+        /// </summary>
+        [JsonPropertyName("file_size")]
+        public long FileSize { get; set; }
+    }
+}

+ 222 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Services/FileUploadService.cs

@@ -0,0 +1,222 @@
+using Quadarax.Application.TemporarySharedStorage.Client.Dtos.Request;
+
+namespace Quadarax.Application.TemporarySharedStorage.Client.Services
+{
+    /// <summary>
+    /// High-level service for uploading files to the QDR Temporary Shared Storage WordPress plugin.
+    /// Provides a simplified interface that orchestrates the complete upload process and handles
+    /// all the complexity of chunked uploads, progress reporting, and error handling.
+    /// 
+    /// This service:
+    /// - Validates files before uploading
+    /// - Manages the 3-phase chunked upload process automatically
+    /// - Provides real-time progress reporting
+    /// - Handles all error scenarios gracefully
+    /// - Formats and displays results in a user-friendly way
+    /// </summary>
+    public class FileUploadService
+    {
+        private readonly QdrTssRestClient _restClient;
+        
+        /// <summary>
+        /// Initializes a new instance of the FileUploadService.
+        /// </summary>
+        /// <param name="baseUrl">The base URL of the WordPress site</param>
+        /// <param name="apiKey">The API key for authentication</param>
+        public FileUploadService(string baseUrl, string apiKey)
+        {
+            _restClient = new QdrTssRestClient(baseUrl, apiKey);
+        }
+        
+        /// <summary>
+        /// Uploads a file to the WordPress plugin using the chunked upload process.
+        /// This method handles the entire upload workflow from start to finish.
+        /// </summary>
+        /// <param name="request">Complete upload request with file path and all metadata</param>
+        /// <returns>A task representing the asynchronous upload operation</returns>
+        /// <remarks>
+        /// The upload process consists of:
+        /// 1. File validation (existence, size)
+        /// 2. Upload initialization with the server
+        /// 3. Chunked file upload with progress reporting
+        /// 4. Upload finalization and result display
+        /// 
+        /// Progress is reported to the console in real-time showing percentage completion.
+        /// All errors are caught and displayed in a user-friendly format.
+        /// </remarks>
+        public async Task UploadFileAsync(UploadFileRequest request)
+        {
+            try
+            {
+                Console.WriteLine($"Starting upload for {request.FilePath}");
+                
+                // Step 1: Validate the local file
+                var fileInfo = new FileInfo(request.FilePath);
+                if (!fileInfo.Exists)
+                {
+                    Console.WriteLine("Error: File not found");
+                    return;
+                }
+                
+                Console.WriteLine($"File: {fileInfo.Name}, Size: {FormatFileSize(fileInfo.Length)}");
+                
+                // Step 2: Initialize the upload with the server
+                var initRequest = new InitUploadRequest
+                {
+                    OriginalFileName = fileInfo.Name,
+                    FileSize = fileInfo.Length
+                };
+                
+                var initResponse = await _restClient.InitializeUploadAsync(initRequest);
+                if (initResponse == null || string.IsNullOrEmpty(initResponse.UploadId))
+                {
+                    Console.WriteLine("Error: Failed to initialize upload");
+                    return;
+                }
+                
+                Console.WriteLine($"Upload initialized with ID: {initResponse.UploadId}");
+                
+                // Step 3: Upload the file in chunks with progress reporting
+                int totalChunks = (int)Math.Ceiling((double)fileInfo.Length / request.ChunkSize);
+                Console.WriteLine($"Total chunks to upload: {totalChunks}");
+                
+                bool uploadSuccess = await UploadFileChunksAsync(
+                    request.FilePath, 
+                    initResponse.UploadId, 
+                    totalChunks, 
+                    request.ChunkSize);
+                
+                if (!uploadSuccess)
+                {
+                    Console.WriteLine("\nError: Failed to upload chunks");
+                    return;
+                }
+                
+                Console.WriteLine("\nAll chunks uploaded successfully");
+                
+                // Step 4: Finalize the upload and create the shared file record
+                Console.WriteLine("Finalizing upload...");
+                
+                var finalizeRequest = new FinalizeUploadRequest
+                {
+                    UploadId = initResponse.UploadId,
+                    TotalChunks = totalChunks,
+                    OriginalFileName = fileInfo.Name,
+                    Description = request.Description,
+                    Message = request.Message,
+                    Password = request.Password,
+                    Reference = request.Reference,
+                    // Use provided dates or sensible defaults
+                    ActiveFrom = request.ActiveFrom?.ToString("yyyy-MM-dd HH:mm:ss") ?? DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
+                    ActiveTo = request.ActiveTo?.ToString("yyyy-MM-dd HH:mm:ss") ?? DateTime.Now.AddDays(30).ToString("yyyy-MM-dd HH:mm:ss")
+                };
+                
+                var finalizeResponse = await _restClient.FinalizeUploadAsync(finalizeRequest);
+                
+                // Step 5: Display results
+                if (finalizeResponse != null)
+                {
+                    Console.WriteLine("\nFile uploaded successfully!");
+                    Console.WriteLine($"File ID: {finalizeResponse.FileId}");
+                    Console.WriteLine($"Permalink: {finalizeResponse.Permalink}");
+                    Console.WriteLine($"Shortcode: {finalizeResponse.Shortcode}");
+                }
+                else
+                {
+                    Console.WriteLine("\nError: Failed to finalize upload");
+                }
+            }
+            catch (Exception ex)
+            {
+                Console.WriteLine($"\nError: {ex.Message}");
+                if (ex.InnerException != null)
+                {
+                    Console.WriteLine($"Inner Exception: {ex.InnerException.Message}");
+                }
+            }
+        }
+        
+        /// <summary>
+        /// Uploads all chunks of a file sequentially with progress reporting.
+        /// This is an internal method that handles the actual file reading and chunk uploading.
+        /// </summary>
+        /// <param name="filePath">Path to the local file to upload</param>
+        /// <param name="uploadId">The upload session ID from initialization</param>
+        /// <param name="totalChunks">Total number of chunks to upload</param>
+        /// <param name="chunkSize">Size of each chunk in bytes</param>
+        /// <returns>True if all chunks uploaded successfully, false otherwise</returns>
+        /// <remarks>
+        /// This method:
+        /// - Reads the file sequentially in chunks to minimize memory usage
+        /// - Uploads chunks one at a time (no parallel uploading to avoid overwhelming the server)
+        /// - Updates console progress display for each chunk
+        /// - Handles the last chunk which may be smaller than the chunk size
+        /// - Stops immediately if any chunk upload fails
+        /// </remarks>
+        private async Task<bool> UploadFileChunksAsync(
+            string filePath, 
+            string uploadId, 
+            int totalChunks, 
+            int chunkSize)
+        {
+            using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
+            byte[] buffer = new byte[chunkSize];
+            
+            for (int i = 0; i < totalChunks; i++)
+            {
+                // Read the next chunk from the file
+                int bytesRead = await fileStream.ReadAsync(buffer, 0, chunkSize);
+                
+                if (bytesRead > 0)
+                {
+                    // Handle the last chunk which may be smaller than chunkSize
+                    byte[] chunkData = buffer;
+                    if (bytesRead < chunkSize)
+                    {
+                        chunkData = new byte[bytesRead];
+                        Array.Copy(buffer, chunkData, bytesRead);
+                    }
+                    
+                    // Upload this chunk to the server
+                    var chunkResponse = await _restClient.UploadChunkAsync(uploadId, i, totalChunks, chunkData);
+                    
+                    if (chunkResponse == null || chunkResponse.Status != "chunk_uploaded")
+                    {
+                        Console.WriteLine($"\nError uploading chunk {i}");
+                        return false;
+                    }
+                    
+                    // Update progress display
+                    double progressPercent = ((i + 1.0) / totalChunks) * 100.0;
+                    Console.Write($"\rUploading: {progressPercent:F2}% complete");
+                }
+            }
+            
+            return true;
+        }
+        
+        /// <summary>
+        /// Formats a file size in bytes into a human-readable string with appropriate units.
+        /// </summary>
+        /// <param name="bytes">The file size in bytes</param>
+        /// <returns>Formatted string like "1.5 MB" or "250 KB"</returns>
+        /// <remarks>
+        /// Uses binary units (1024-based) rather than decimal units (1000-based).
+        /// Rounds to 2 decimal places for readability.
+        /// </remarks>
+        private string FormatFileSize(long bytes)
+        {
+            string[] sizes = { "B", "KB", "MB", "GB", "TB" };
+            int order = 0;
+            double size = bytes;
+            
+            while (size >= 1024 && order < sizes.Length - 1)
+            {
+                order++;
+                size /= 1024;
+            }
+            
+            return $"{size:0.##} {sizes[order]}";
+        }
+    }
+}

+ 290 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/TssRestClient .cs

@@ -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();
+        }
+    }
+}

+ 14 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/qdr.app.tss.client.csproj

@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net9.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <RootNamespace>Quadarax.Application.TemporarySharedStorage.Client</RootNamespace>
+    <Title>Quadarax Temporary Shared Storage REST API client</Title>
+    <Description>Quadarax Temporary Shared Storage REST API client</Description>
+    <Copyright>Copyright © Quadarax 2025</Copyright>
+    <PackageProjectUrl>https://www.quadarax.com/plugins/qdr-temporary-shared-storage</PackageProjectUrl>
+  </PropertyGroup>
+
+</Project>

+ 7 - 10
qdr-temporary-shared-storage/pack.cmd

@@ -5,18 +5,13 @@ echo QDR Temporary Shared Storage Plugin Packager
 echo ===========================================
 echo.
 
-:: Get plugin name from parent directory
-for %%I in ("%CD%") do set PLUGIN_NAME=%%~nxI
-
-:: Set other variables
+:: Set variables
+set PLUGIN_NAME=qdr-temporary-shared-storage
 set DEPLOY_DIR=D:\Deploy
 set CURRENT_DIR=%CD%
 set TARGET_ZIP=%DEPLOY_DIR%\%PLUGIN_NAME%.zip
 set SCRIPT_NAME=%~nx0
 
-echo Plugin name detected as: %PLUGIN_NAME%
-echo.
-
 :: Create deploy directory if it doesn't exist
 if not exist "%DEPLOY_DIR%" (
     echo Creating deploy directory %DEPLOY_DIR%...
@@ -30,7 +25,7 @@ if exist "%TARGET_ZIP%" (
 )
 
 :: Create temporary directory structure for packaging
-set TEMP_DIR=%TEMP%\plugin_package
+set TEMP_DIR=%TEMP%\%PLUGIN_NAME%_package
 set PLUGIN_TEMP_DIR=%TEMP_DIR%\%PLUGIN_NAME%
 
 :: Clean up any existing temporary directories
@@ -43,7 +38,9 @@ echo Copying plugin files to temporary directory...
 :: Copy all files and directories except excluded ones
 for /d %%D in (*) do (
     if /i not "%%D"=="@documentation" (
-        xcopy "%%D" "%PLUGIN_TEMP_DIR%\%%D\" /E /I /H /Y >nul
+        if /i not "%%D"=="@client" (
+            xcopy "%%D" "%PLUGIN_TEMP_DIR%\%%D\" /E /I /H /Y >nul
+        )
     )
 )
 
@@ -75,4 +72,4 @@ if exist "%TARGET_ZIP%" (
 
 echo.
 echo Process complete.
-endlocal
+endlocal