Quellcode durchsuchen

qdr-temporary-shared-storage: finalize cli console

Dalibor Votruba vor 1 Jahr
Ursprung
Commit
45c3630c67
14 geänderte Dateien mit 1008 neuen und 24 gelöschten Zeilen
  1. 21 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/directory-structure.txt
  2. 26 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client.console/CommandContext.cs
  3. 68 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client.console/Commands/BaseCmd.cs
  4. 144 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client.console/Commands/CmdUpload.cs
  5. 23 1
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client.console/Program.cs
  6. 8 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client.console/qdr.app.tss.client.console.csproj
  7. 28 23
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Services/FileUploadService.cs
  8. 29 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/bootstrap.cmd
  9. 15 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/buildD.cmd
  10. 15 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/buildR.cmd
  11. 16 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/publishD.cmd
  12. 16 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/publishR.cmd
  13. 4 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/qdr.app.tss.client.csproj
  14. 595 0
      qdr-temporary-shared-storage/@client/qdr.app.tss.client/rest-api-docs.md

+ 21 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/directory-structure.txt

@@ -0,0 +1,21 @@
+\qdr.app.tss.client\qdr.app.tss.client
+\qdr.app.tss.client\qdr.app.tss.client.console
+\qdr.app.tss.client\qdr.app.tss.client.sln
+\qdr.app.tss.client\qdr.app.tss.client\Dtos
+\qdr.app.tss.client\qdr.app.tss.client\Services
+\qdr.app.tss.client\qdr.app.tss.client\qdr.app.tss.client.csproj
+\qdr.app.tss.client\qdr.app.tss.client\TssRestClient.cs
+\qdr.app.tss.client\qdr.app.tss.client\Dtos\Request
+\qdr.app.tss.client\qdr.app.tss.client\Dtos\Response
+\qdr.app.tss.client\qdr.app.tss.client\Dtos\Request\FinalizeUploadRequest.cs
+\qdr.app.tss.client\qdr.app.tss.client\Dtos\Request\InitUploadRequest.cs
+\qdr.app.tss.client\qdr.app.tss.client\Dtos\Request\UpdateMediaRequest.cs
+\qdr.app.tss.client\qdr.app.tss.client\Dtos\Request\UploadFileRequest.cs
+\qdr.app.tss.client\qdr.app.tss.client\Dtos\Response\ApiErrorResponse.cs
+\qdr.app.tss.client\qdr.app.tss.client\Dtos\Response\DeleteMediaResponse.cs
+\qdr.app.tss.client\qdr.app.tss.client\Dtos\Response\FinalizeUploadResponse.cs
+\qdr.app.tss.client\qdr.app.tss.client\Dtos\Response\ChunkUploadResponse.cs
+\qdr.app.tss.client\qdr.app.tss.client\Dtos\Response\InitUploadResponse.cs
+\qdr.app.tss.client\qdr.app.tss.client\Dtos\Response\MediaItemResponse.cs
+\qdr.app.tss.client\qdr.app.tss.client.console\Program.cs
+\qdr.app.tss.client\qdr.app.tss.client.console\qdr.app.tss.client.console.csproj

+ 26 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client.console/CommandContext.cs

@@ -0,0 +1,26 @@
+using Quadarax.Foundation.Core.Object;
+using Quadarax.Foundation.Core.QConsole.Context;
+using System.IO.Abstractions;
+
+namespace qdr.app.tss.client.console
+{
+    internal class CommandContext : DisposableObject, ICommandContext
+    {
+
+        private IFileSystem? _fileSystem;
+
+        public IFileSystem FileSystem => _fileSystem ?? new FileSystem();
+
+        public string Status => "READY";
+
+        public CommandContext(IFileSystem fileSystem)
+        {
+            _fileSystem = fileSystem;
+        }
+
+        protected override void OnDisposing()
+        {
+            _fileSystem = null;
+        }
+    }
+}

+ 68 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client.console/Commands/BaseCmd.cs

@@ -0,0 +1,68 @@
+using qdr.app.tss.client.console;
+using Quadarax.Foundation.Core.QConsole;
+using Quadarax.Foundation.Core.QConsole.Argument;
+using Quadarax.Foundation.Core.QConsole.Command.Base;
+using System.IO.Abstractions;
+
+namespace qdr.app.qbstack.Commands
+{
+    abstract class BaseCmd : AbstractCommand
+    {
+        #region *** Constants ***
+        private const string ARG_URL_NAME = "url";
+        private const string ARG_URL_HINT = "url";
+        private const string ARG_URL_DESCR = "The WordPress site URL where TSS plugin installed (e.g. https://example.com).";
+
+        private const string ARG_APIKEY_NAME = "apiKey";
+        private const string ARG_APIKEY_HINT = "api_key";
+        private const string ARG_APIKEY_DESCR = "API key for authentication (can be obtain from TSS plugin setting).";
+        #endregion
+
+
+        #region *** Private fields ***
+   
+        #endregion
+
+        #region *** Properties ***
+        protected IFileSystem FileSystem => GetContext<CommandContext>().FileSystem;
+        protected Uri Url {get; private set;} = new Uri("https://example.com/");
+        protected string ApiKey {get; private set;} = string.Empty;
+        #endregion
+
+
+        #region *** Constructors ***
+        protected BaseCmd(Engine engine) : base(engine)
+        {
+        }
+        #endregion
+
+
+        #region *** Operations ***
+        
+        #endregion
+
+        #region *** Overrides ***
+        protected override IEnumerable<AbstractArgument> OnSetupArguments()
+        {
+            var list = base.OnSetupArguments().ToList();
+            list.Add(new NamedArgument(ARG_URL_NAME, ARG_URL_DESCR, ARG_URL_HINT, Quadarax.Foundation.Core.QConsole.Value.TypeValuesEnum.String, string.Empty, true));
+            list.Add(new NamedArgument(ARG_APIKEY_NAME, ARG_APIKEY_DESCR, ARG_APIKEY_HINT, Quadarax.Foundation.Core.QConsole.Value.TypeValuesEnum.String, string.Empty,true));
+            return list;
+        }
+      
+        protected override void OnValidateArguments()
+        {
+            base.OnValidateArguments();
+
+            var url = GetArgumentValueOrDefault<string>(ARG_URL_NAME);
+            if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
+                throw new ArgumentException($"URL '{url}' not valid.", ARG_URL_NAME);
+
+            Url = uri;
+
+            ApiKey = GetArgumentValueOrDefault<string>(ARG_APIKEY_NAME)!;
+           
+        }
+        #endregion
+    }
+}

+ 144 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client.console/Commands/CmdUpload.cs

@@ -0,0 +1,144 @@
+using Quadarax.Application.TemporarySharedStorage.Client.Dtos.Request;
+using Quadarax.Application.TemporarySharedStorage.Client.Services;
+using Quadarax.Foundation.Core.Value.Extensions;
+using Quadarax.Foundation.Core.Logging;
+using Quadarax.Foundation.Core.QConsole;
+using Quadarax.Foundation.Core.QConsole.Argument;
+using Quadarax.Foundation.Core.QConsole.Attributes;
+using Quadarax.Foundation.Core.QConsole.Value;
+using Quadarax.Foundation.Core.Value;
+
+namespace qdr.app.qbstack.Commands
+{
+    [CommandDefinition]
+    internal class CmdUpload : BaseCmd
+    {
+
+        #region *** Constants ***
+        private const string CS_CMD_NAME = "upload";
+        private const string CS_CMD_DESCR = "Upload a file to QDR Temporary Shared Storage.";
+
+        private const string ARG_FILE_NAME = "filePath";
+        private const string ARG_FILE_HINT = "file_path";
+        private const string ARG_FILE_DESCR = "Path to the file to upload.";
+
+        private const string ARG_DESCR_NAME = "descr";
+        private const string ARG_DESCR_HINT = "description";
+        private const string ARG_DESCR_DESCR = "Description of uploading file.";
+
+        private const string ARG_PWD_NAME = "pwd";
+        private const string ARG_PWD_HINT = "password";
+        private const string ARG_PWD_DESCR = "Password for download file.";
+
+        private const string ARG_MSG_NAME = "msg";
+        private const string ARG_MSG_HINT = "message";
+        private const string ARG_MSG_DESCR = "Message for download file.";
+
+        private const string ARG_REF_NAME = "ref";
+        private const string ARG_REF_HINT = "reference";
+        private const string ARG_REF_DESCR = "Custom reference value for download file.";
+
+        private const string ARG_AFROM_NAME = "activeFrom";
+        private const string ARG_AFROM_HINT = "active_from";
+        private const string ARG_AFROM_DESCR = "Date and time from which the file will be active (format: \"yyyy-MM-dd HH:mm:ss\").";
+
+        private const string ARG_ATO_NAME = "activeTo";
+        private const string ARG_ATO_HINT = "active_to";
+        private const string ARG_ATO_DESCR = "Date and time until which the file will be active (format: \"yyyy-MM-dd HH:mm:ss\").";
+
+        private const string ARG_CHSIZE_NAME = "chunckSize";
+        private const string ARG_CHSIZE_HINT = "chunck_size";
+        private const string ARG_CHSIZE_DESCR = "Size of chunks in bytes for uploading.";
+
+       
+        #endregion
+
+        public override string Name => CS_CMD_NAME;
+        public override string Description => CS_CMD_DESCR;
+
+    
+
+        protected string FileName {get;private set;} = string.Empty;
+        protected string FileDescription {get;private set;} = string.Empty;
+        protected string Message {get;private set;} = string.Empty;
+        protected string Password {get;private set;} = string.Empty;
+        protected string Reference {get;private set;} = string.Empty;
+        protected int ChunckSize {get;private set;} = 1048576;
+        protected DateTime ActiveFrom {get;private set;} = DateTime.MinValue;
+        protected DateTime ActiveTo {get;private set;} = DateTime.MinValue;
+
+        #region *** Constructors ***
+        public CmdUpload(Engine engine) : base(engine)
+        {
+        }
+        #endregion
+
+        #region *** Overrides ***
+
+        protected override void OnValidateArguments()
+        {
+            base.OnValidateArguments();
+
+            var file = GetArgumentValueOrDefault<string>(ARG_FILE_NAME);
+            if (!FileSystem.File.Exists(file))
+            {
+                throw new FileNotFoundException($"File not found: {file}",file);
+            }
+            FileName = file;
+            FileDescription = GetArgumentValueOrDefault<string>(ARG_DESCR_NAME)!;
+            Password = GetArgumentValueOrDefault<string>(ARG_PWD_NAME)!;
+            Reference = GetArgumentValueOrDefault<string>(ARG_REF_NAME)!;
+            Message = GetArgumentValueOrDefault<string>(ARG_MSG_NAME)!;
+            ChunckSize = GetArgumentValueOrDefault<int>(ARG_CHSIZE_NAME);
+            if (ChunckSize <= 0)
+            {
+                throw new ArgumentException($"Chunk size must be greater than 0.", ARG_CHSIZE_NAME);
+            }
+            ActiveFrom = GetArgumentValueOrDefault<DateTime>(ARG_AFROM_NAME);
+            ActiveTo = GetArgumentValueOrDefault<DateTime>(ARG_ATO_NAME);
+            if (ActiveFrom >= ActiveTo)
+            {
+                throw new ArgumentException($"Active from date must be earlier than active to date.", ARG_AFROM_NAME);
+            }
+
+        }
+
+        protected override IEnumerable<AbstractArgument> OnSetupArguments()
+        {
+            var list = base.OnSetupArguments().ToList();
+            list.Add(new NamedArgument(ARG_FILE_NAME, ARG_FILE_DESCR, ARG_FILE_HINT, TypeValuesEnum.String, string.Empty, true));
+            list.Add(new NamedArgument(ARG_DESCR_NAME, ARG_DESCR_DESCR, ARG_DESCR_HINT, TypeValuesEnum.String, string.Empty, false));
+            list.Add(new NamedArgument(ARG_PWD_NAME, ARG_PWD_DESCR, ARG_PWD_HINT, TypeValuesEnum.String, string.Empty, true));
+            list.Add(new NamedArgument(ARG_REF_NAME, ARG_REF_DESCR, ARG_REF_HINT, TypeValuesEnum.String, string.Empty, true));
+            list.Add(new NamedArgument(ARG_AFROM_NAME, ARG_AFROM_DESCR, ARG_AFROM_HINT, TypeValuesEnum.DateTime, string.Empty, true));
+            list.Add(new NamedArgument(ARG_ATO_NAME, ARG_ATO_DESCR, ARG_ATO_HINT, TypeValuesEnum.DateTime, string.Empty, true));
+            list.Add(new NamedArgument(ARG_MSG_NAME, ARG_MSG_DESCR, ARG_MSG_HINT, TypeValuesEnum.String, string.Empty, true));
+            list.Add(new NamedArgument(ARG_CHSIZE_NAME, ARG_CHSIZE_DESCR, ARG_CHSIZE_HINT, TypeValuesEnum.Integer, "0", false));
+            return list;
+        }
+
+        protected override Result OnExecute()
+        {
+            var srv = new FileUploadService(Url.ToString(), ApiKey, new ConsoleLogger(LogSeverityEnum.Info, LogSeverityEnum.Debug, LogSeverityEnum.Error));
+            var uploadRequest = new UploadFileRequest
+                {
+                    FilePath = FileName,
+                    Description = FileDescription,
+                    Password = Password,
+                    Message = Message,
+                    Reference = Reference,
+                    ActiveFrom = ActiveFrom,
+                    ActiveTo = ActiveTo,
+                    ChunkSize = ChunckSize
+                };
+                
+            Task.Run(() => srv.UploadFileAsync(uploadRequest)).Wait();
+
+
+            return new Result();
+        }
+
+       
+        #endregion
+    }
+}

+ 23 - 1
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client.console/Program.cs

@@ -1,2 +1,24 @@
 // See https://aka.ms/new-console-template for more information
-Console.WriteLine("Hello, World!");
+using System.Reflection;
+using Quadarax.Foundation.Core.QConsole;
+using Quadarax.Foundation.Core.QConsole.Configuration;
+using Quadarax.Foundation.Core.QConsole.Context;
+using Quadarax.Foundation.Core.Reflection.Extensions;
+
+var consoleVersion = Assembly.GetExecutingAssembly()?.GetName()?.Version;
+if (consoleVersion != null)
+{
+    var config = new StartupConfiguration("tss.client.console", consoleVersion)
+    {
+        ConsoleCopyright = Assembly.GetExecutingAssembly().GetCopyright(),
+        AllowInteractive = false,
+        WaitOnKeyInNonInteractiveMode = false,
+        CommandDefinitionAssembly = new Assembly[] { Assembly.GetEntryAssembly()! }
+    };
+    config.DisableDefaultCommands();
+    config.ShowStatusOnEveryCommand = false;
+    config.IsConsoleDebug = false;
+
+    var console = new Engine(config, new DefualtEngineContext(), args);
+    console.Start();
+}

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

@@ -7,4 +7,12 @@
     <Nullable>enable</Nullable>
   </PropertyGroup>
 
+  <ItemGroup>
+    <PackageReference Include="qdr.fnd.core.qconsole" Version="0.0.7-alpha" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\qdr.app.tss.client\qdr.app.tss.client.csproj" />
+  </ItemGroup>
+
 </Project>

+ 28 - 23
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/Services/FileUploadService.cs

@@ -1,4 +1,5 @@
 using Quadarax.Application.TemporarySharedStorage.Client.Dtos.Request;
+using Quadarax.Foundation.Core.Logging;
 
 namespace Quadarax.Application.TemporarySharedStorage.Client.Services
 {
@@ -14,17 +15,20 @@ namespace Quadarax.Application.TemporarySharedStorage.Client.Services
     /// - Handles all error scenarios gracefully
     /// - Formats and displays results in a user-friendly way
     /// </summary>
-    public class FileUploadService
+    public class FileUploadService : ILogHandler
     {
         private readonly QdrTssRestClient _restClient;
-        
+        private ILog _log;
+
+
         /// <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)
+        public FileUploadService(string baseUrl, string apiKey, ILogger logger)
         {
+            _log = logger.GetLogger(typeof(FileUploadService));
             _restClient = new QdrTssRestClient(baseUrl, apiKey);
         }
         
@@ -48,17 +52,17 @@ namespace Quadarax.Application.TemporarySharedStorage.Client.Services
         {
             try
             {
-                Console.WriteLine($"Starting upload for {request.FilePath}");
+                Log(LogSeverityEnum.Info, $"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");
+                    Log(LogSeverityEnum.Error, $"File '{request.FilePath}' not found");
                     return;
                 }
                 
-                Console.WriteLine($"File: {fileInfo.Name}, Size: {FormatFileSize(fileInfo.Length)}");
+                Log(LogSeverityEnum.Info, $"File: {fileInfo.Name}, Size: {FormatFileSize(fileInfo.Length)}");
                 
                 // Step 2: Initialize the upload with the server
                 var initRequest = new InitUploadRequest
@@ -70,7 +74,7 @@ namespace Quadarax.Application.TemporarySharedStorage.Client.Services
                 var initResponse = await _restClient.InitializeUploadAsync(initRequest);
                 if (initResponse == null || string.IsNullOrEmpty(initResponse.UploadId))
                 {
-                    Console.WriteLine("Error: Failed to initialize upload");
+                    Log(LogSeverityEnum.Error, "Failed to initialize upload");
                     return;
                 }
                 
@@ -78,7 +82,7 @@ namespace Quadarax.Application.TemporarySharedStorage.Client.Services
                 
                 // 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}");
+                Log(LogSeverityEnum.Debug, $"Total chunks to upload: {totalChunks}");
                 
                 bool uploadSuccess = await UploadFileChunksAsync(
                     request.FilePath, 
@@ -88,14 +92,14 @@ namespace Quadarax.Application.TemporarySharedStorage.Client.Services
                 
                 if (!uploadSuccess)
                 {
-                    Console.WriteLine("\nError: Failed to upload chunks");
+                    Log(LogSeverityEnum.Error, "Failed to upload chunks");
                     return;
                 }
                 
-                Console.WriteLine("\nAll chunks uploaded successfully");
+                 Log(LogSeverityEnum.Info, "All chunks uploaded successfully");
                 
                 // Step 4: Finalize the upload and create the shared file record
-                Console.WriteLine("Finalizing upload...");
+                Log(LogSeverityEnum.Debug,"Finalizing upload...");
                 
                 var finalizeRequest = new FinalizeUploadRequest
                 {
@@ -116,23 +120,19 @@ namespace Quadarax.Application.TemporarySharedStorage.Client.Services
                 // 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}");
+                    Log(LogSeverityEnum.Info, "File uploaded successfully!");
+                    Log(LogSeverityEnum.Debug, $"File ID: {finalizeResponse.FileId}");
+                    Log(LogSeverityEnum.Debug, $"Permalink: {finalizeResponse.Permalink}");
+                    Log(LogSeverityEnum.Debug, $"Shortcode: {finalizeResponse.Shortcode}");
                 }
                 else
                 {
-                    Console.WriteLine("\nError: Failed to finalize upload");
+                    Log(LogSeverityEnum.Error,"Failed to finalize upload");
                 }
             }
             catch (Exception ex)
             {
-                Console.WriteLine($"\nError: {ex.Message}");
-                if (ex.InnerException != null)
-                {
-                    Console.WriteLine($"Inner Exception: {ex.InnerException.Message}");
-                }
+                Log(LogSeverityEnum.Error,"Internal error. See exception:", ex);
             }
         }
         
@@ -182,13 +182,13 @@ namespace Quadarax.Application.TemporarySharedStorage.Client.Services
                     
                     if (chunkResponse == null || chunkResponse.Status != "chunk_uploaded")
                     {
-                        Console.WriteLine($"\nError uploading chunk {i}");
+                        Log(LogSeverityEnum.Debug, $"\nError uploading chunk {i}");
                         return false;
                     }
                     
                     // Update progress display
                     double progressPercent = ((i + 1.0) / totalChunks) * 100.0;
-                    Console.Write($"\rUploading: {progressPercent:F2}% complete");
+                    Log(LogSeverityEnum.Info, $"Uploading: {progressPercent:F2}% complete");
                 }
             }
             
@@ -218,5 +218,10 @@ namespace Quadarax.Application.TemporarySharedStorage.Client.Services
             
             return $"{size:0.##} {sizes[order]}";
         }
+
+        public void Log(LogSeverityEnum type, string message, Exception? e = null)
+        {
+            _log?.Log(type, message, e);
+        }
     }
 }

+ 29 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/bootstrap.cmd

@@ -0,0 +1,29 @@
+@echo off
+echo * Bootstrap started to setup environment
+set nuget=D:\Projects\nuget.exe
+set nugetrepo=D:\Projects\quadarax\@nuget.repo
+
+rem ** check if directories exists **
+if not exist "bin" goto err_bin_not_found
+if not exist "%nuget%" goto err_locnuget_not_found
+if not exist "%nugetrepo%" goto err_locnugetrepo_not_found
+
+
+goto done
+
+:err_bin_not_found
+echo ERR: missing 'bin' directory in current location
+goto exit
+
+:err_locnuget_not_found
+echo ERR: cannot find nuget.exe '%nuget%' or path is invalid.
+goto exit
+
+:err_locnugetrepo_not_found
+echo ERR: cannot find nuget repository '%nugetrepo%' or path is invalid.
+goto exit
+
+
+:done
+echo * Bootstrap succesfuly set
+:exit

+ 15 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/buildD.cmd

@@ -0,0 +1,15 @@
+@echo off
+call bootstrap.cmd
+
+rem ** cleanup **
+rmdir /s /q bin\debug
+
+rem ** build **
+dotnet build -c Debug
+
+
+goto done
+
+:done
+echo *** DONE ***
+:exit

+ 15 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/buildR.cmd

@@ -0,0 +1,15 @@
+@echo off
+call bootstrap.cmd
+
+rem ** cleanup **
+rmdir /s /q bin\release
+
+rem ** build **
+dotnet build -c Release
+
+
+goto done
+
+:done
+echo *** DONE ***
+:exit

+ 16 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/publishD.cmd

@@ -0,0 +1,16 @@
+@echo off
+call bootstrap.cmd
+
+rem ** cleanup **
+rmdir /s /q bin\debug
+
+rem ** build **
+dotnet pack -c Debug 
+
+for %%i in (bin\debug\*.nupkg) do nuget add %%i -source %nugetrepo%
+
+goto done
+
+:done
+echo *** DONE ***
+:exit

+ 16 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/qdr.app.tss.client/publishR.cmd

@@ -0,0 +1,16 @@
+@echo off
+call bootstrap.cmd
+
+rem ** cleanup **
+rmdir /s /q bin\release
+
+rem ** build **
+dotnet pack -c Release
+
+for %%i in (bin\release\*.nupkg) do nuget add %%i -source %nugetrepo%
+
+goto done
+
+:done
+echo *** DONE ***
+:exit

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

@@ -11,4 +11,8 @@
     <PackageProjectUrl>https://www.quadarax.com/plugins/qdr-temporary-shared-storage</PackageProjectUrl>
   </PropertyGroup>
 
+  <ItemGroup>
+    <PackageReference Include="qdr.fnd.core" Version="0.0.8-alpha" />
+  </ItemGroup>
+
 </Project>

+ 595 - 0
qdr-temporary-shared-storage/@client/qdr.app.tss.client/rest-api-docs.md

@@ -0,0 +1,595 @@
+# QDR Temporary Shared Storage - REST API Documentation
+
+## Introduction
+
+The QDR Temporary Shared Storage plugin provides a comprehensive REST API for uploading, managing, and downloading temporary shared files. This document outlines the available endpoints, authentication requirements, and provides examples in both PHP and JavaScript.
+
+## Authentication
+
+All API requests require an API key sent in the `X-Api-Key` HTTP header. You can generate or view your API key in the plugin settings page at "Settings > Shared Storage".
+
+## API Base URL
+
+The base URL for all API endpoints is:
+
+```
+https://your-wordpress-site.com/wp-json/qdr-tss/v1/
+```
+
+## Endpoints
+
+### 1. Initialize Chunked Upload
+
+Prepares the server for a chunked upload.
+
+- **Endpoint**: `/upload/init`
+- **Method**: `POST`
+- **Parameters**:
+  - `original_file_name`: The original name of the file being uploaded
+  - `file_size`: The total size of the file in bytes
+
+- **Response**:
+  ```json
+  {
+    "upload_id": "5f8d7a6b9c3e",
+    "status": "initialized"
+  }
+  ```
+
+### 2. Upload Chunk
+
+Uploads a single chunk of the file.
+
+- **Endpoint**: `/upload/chunk`
+- **Method**: `POST`
+- **Parameters**:
+  - `upload_id`: The upload ID received from the initialization
+  - `chunk_index`: The index of the current chunk (starting from 0)
+  - `total_chunks`: The total number of chunks
+  - `chunk`: The chunk file data (multipart/form-data)
+
+- **Response**:
+  ```json
+  {
+    "upload_id": "5f8d7a6b9c3e",
+    "chunk_index": 0,
+    "status": "chunk_uploaded"
+  }
+  ```
+
+### 3. Finalize Upload
+
+Combines all chunks and creates the file record.
+
+- **Endpoint**: `/upload/finalize`
+- **Method**: `POST`
+- **Parameters**:
+  - `upload_id`: The upload ID received from the initialization
+  - `total_chunks`: The total number of chunks
+  - `original_file_name`: The original name of the file
+  - `description`: Description of the file
+  - `message`: Custom message (optional)
+  - `active_from`: Date from which permalink will be active (optional, defaults to current time)
+  - `active_to`: Date until which permalink will be active (optional, defaults to 30 days from now)
+  - `password`: Password for download protection
+  - `reference`: Custom reference value (optional)
+
+- **Response**:
+  ```json
+  {
+    "media_id": "1621234567",
+    "permalink": "https://your-wordpress-site.com/shared-file-download/1621234567",
+    "shortcode": "[qdr_tss_download id=\"1621234567\"]"
+  }
+  ```
+
+### 4. Get File Details
+
+Retrieves details about a specific file.
+
+- **Endpoint**: `/media/{media_id}`
+- **Method**: `GET`
+
+- **Response**:
+  ```json
+  {
+    "id": 1,
+    "media_id": "1621234567",
+    "original_file_name": "example.pdf",
+    "description": "Example document",
+    "message": "Please review this document",
+    "active_from": "2025-05-19 15:30:00",
+    "active_to": "2025-06-18 15:30:00",
+    "reference": "REF123",
+    "upload_date": "2025-05-19 15:30:00",
+    "download_count": 5,
+    "last_download": "2025-05-20 10:45:30",
+    "file_size": 1048576
+  }
+  ```
+
+### 5. Update File
+
+Updates properties of a specific file.
+
+- **Endpoint**: `/media/{media_id}`
+- **Method**: `PUT`
+- **Parameters**:
+  - `message`: Custom message (optional)
+  - `active_from`: Date from which permalink will be active (optional)
+  - `active_to`: Date until which permalink will be active (optional)
+  - `reference`: Custom reference value (optional)
+  - `description`: File description (optional)
+
+- **Response**: Updated file details (same format as Get File Details)
+
+### 6. Delete File
+
+Deletes a specific file.
+
+- **Endpoint**: `/media/{media_id}`
+- **Method**: `DELETE`
+
+- **Response**:
+  ```json
+  {
+    "deleted": true,
+    "media_id": "1621234567"
+  }
+  ```
+
+### 7. List Files
+
+Retrieves a list of files with pagination and filtering.
+
+- **Endpoint**: `/media`
+- **Method**: `GET`
+- **Parameters**:
+  - `page`: Page number (optional, default: 1)
+  - `per_page`: Items per page (optional, default: 10)
+  - `orderby`: Field to sort by (optional, default: "upload_date")
+  - `order`: Sort order (optional, default: "DESC")
+  - `original_file_name`: Filter by file name (optional)
+  - `reference`: Filter by reference (optional)
+
+- **Response**:
+  ```json
+  [
+    {
+      "id": 1,
+      "media_id": "1621234567",
+      "original_file_name": "example.pdf",
+      "description": "Example document",
+      "message": "Please review this document",
+      "active_from": "2025-05-19 15:30:00",
+      "active_to": "2025-06-18 15:30:00",
+      "reference": "REF123",
+      "upload_date": "2025-05-19 15:30:00",
+      "download_count": 5,
+      "last_download": "2025-05-20 10:45:30",
+      "file_size": 1048576
+    },
+    ...
+  ]
+  ```
+
+## Code Examples
+
+### PHP Example: Complete File Upload
+
+```php
+<?php
+/**
+ * Example of uploading a file using chunked upload
+ */
+function uploadSharedFile($file_path, $description, $password, $reference = '') {
+    $api_key = 'your-api-key-here';
+    $api_url = 'https://your-wordpress-site.com/wp-json/qdr-tss/v1';
+    
+    // File information
+    $file_name = basename($file_path);
+    $file_size = filesize($file_path);
+    $chunk_size = 1024 * 1024; // 1MB chunks
+    $total_chunks = ceil($file_size / $chunk_size);
+    
+    // Step 1: Initialize upload
+    $ch = curl_init($api_url . '/upload/init');
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+    curl_setopt($ch, CURLOPT_POST, true);
+    curl_setopt($ch, CURLOPT_HTTPHEADER, [
+        'X-Api-Key: ' . $api_key,
+        'Content-Type: application/json'
+    ]);
+    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
+        'original_file_name' => $file_name,
+        'file_size' => $file_size
+    ]));
+    
+    $response = curl_exec($ch);
+    curl_close($ch);
+    
+    $init_result = json_decode($response, true);
+    if (!isset($init_result['upload_id'])) {
+        throw new Exception('Failed to initialize upload: ' . $response);
+    }
+    
+    $upload_id = $init_result['upload_id'];
+    
+    // Step 2: Upload chunks
+    $file = fopen($file_path, 'rb');
+    
+    for ($i = 0; $i < $total_chunks; $i++) {
+        // Create a temporary file for the chunk
+        $chunk_file = tempnam(sys_get_temp_dir(), 'chunk');
+        $chunk_handle = fopen($chunk_file, 'wb');
+        
+        // Read chunk from original file
+        $chunk_data = fread($file, $chunk_size);
+        fwrite($chunk_handle, $chunk_data);
+        fclose($chunk_handle);
+        
+        // Upload the chunk
+        $ch = curl_init($api_url . '/upload/chunk');
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_POST, true);
+        curl_setopt($ch, CURLOPT_HTTPHEADER, [
+            'X-Api-Key: ' . $api_key
+        ]);
+        
+        $post_data = [
+            'upload_id' => $upload_id,
+            'chunk_index' => $i,
+            'total_chunks' => $total_chunks,
+            'chunk' => new CURLFile($chunk_file, 'application/octet-stream', 'chunk')
+        ];
+        
+        curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
+        $response = curl_exec($ch);
+        curl_close($ch);
+        
+        // Delete temporary chunk file
+        unlink($chunk_file);
+        
+        $chunk_result = json_decode($response, true);
+        if (!isset($chunk_result['status']) || $chunk_result['status'] !== 'chunk_uploaded') {
+            throw new Exception('Failed to upload chunk ' . $i . ': ' . $response);
+        }
+        
+        // Display progress
+        echo "Uploaded chunk " . ($i + 1) . " of " . $total_chunks . "\n";
+    }
+    
+    fclose($file);
+    
+    // Step 3: Finalize upload
+    $ch = curl_init($api_url . '/upload/finalize');
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+    curl_setopt($ch, CURLOPT_POST, true);
+    curl_setopt($ch, CURLOPT_HTTPHEADER, [
+        'X-Api-Key: ' . $api_key,
+        'Content-Type: application/json'
+    ]);
+    
+    // Set expiration to 30 days from now
+    $active_to = date('Y-m-d H:i:s', strtotime('+30 days'));
+    
+    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
+        'upload_id' => $upload_id,
+        'total_chunks' => $total_chunks,
+        'original_file_name' => $file_name,
+        'description' => $description,
+        'message' => 'Uploaded via API example',
+        'password' => $password,
+        'reference' => $reference,
+        'active_to' => $active_to
+    ]));
+    
+    $response = curl_exec($ch);
+    curl_close($ch);
+    
+    $final_result = json_decode($response, true);
+    if (!isset($final_result['media_id'])) {
+        throw new Exception('Failed to finalize upload: ' . $response);
+    }
+    
+    return $final_result;
+}
+
+// Example usage
+try {
+    $result = uploadSharedFile(
+        '/path/to/your/file.pdf',
+        'Example document uploaded via API',
+        'secure_password',
+        'REF123'
+    );
+    
+    echo "File uploaded successfully!\n";
+    echo "Media ID: " . $result['media_id'] . "\n";
+    echo "Permalink: " . $result['permalink'] . "\n";
+    echo "Shortcode: " . $result['shortcode'] . "\n";
+} catch (Exception $e) {
+    echo "Error: " . $e->getMessage() . "\n";
+}
+```
+
+### JavaScript Example: Chunked File Upload
+
+```javascript
+/**
+ * Uploads a file using chunked upload
+ * @param {File} file The file to upload
+ * @param {string} description File description
+ * @param {string} password Password for download protection
+ * @param {string} reference Optional reference
+ * @param {function} progressCallback Callback for upload progress
+ * @returns {Promise} Promise resolving to the upload result
+ */
+async function uploadSharedFile(file, description, password, reference = '', progressCallback = null) {
+    const apiKey = 'your-api-key-here';
+    const apiUrl = 'https://your-wordpress-site.com/wp-json/qdr-tss/v1';
+    
+    const chunkSize = 1024 * 1024; // 1MB chunks
+    const totalChunks = Math.ceil(file.size / chunkSize);
+    
+    // Step 1: Initialize upload
+    const initResponse = await fetch(`${apiUrl}/upload/init`, {
+        method: 'POST',
+        headers: {
+            'X-Api-Key': apiKey,
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+            original_file_name: file.name,
+            file_size: file.size
+        })
+    });
+    
+    const initResult = await initResponse.json();
+    if (!initResult.upload_id) {
+        throw new Error('Failed to initialize upload');
+    }
+    
+    const uploadId = initResult.upload_id;
+    
+    // Step 2: Upload chunks
+    for (let i = 0; i < totalChunks; i++) {
+        const start = i * chunkSize;
+        const end = Math.min(file.size, start + chunkSize);
+        const chunk = file.slice(start, end);
+        
+        const formData = new FormData();
+        formData.append('upload_id', uploadId);
+        formData.append('chunk_index', i);
+        formData.append('total_chunks', totalChunks);
+        formData.append('chunk', chunk);
+        
+        const chunkResponse = await fetch(`${apiUrl}/upload/chunk`, {
+            method: 'POST',
+            headers: {
+                'X-Api-Key': apiKey
+            },
+            body: formData
+        });
+        
+        const chunkResult = await chunkResponse.json();
+        if (!chunkResult.status || chunkResult.status !== 'chunk_uploaded') {
+            throw new Error(`Failed to upload chunk ${i}`);
+        }
+        
+        // Update progress
+        if (progressCallback) {
+            progressCallback((i + 1) / totalChunks * 100);
+        }
+    }
+    
+    // Step 3: Finalize upload
+    const activeToDate = new Date();
+    activeToDate.setDate(activeToDate.getDate() + 30); // 30 days from now
+    const activeTo = activeToDate.toISOString().slice(0, 19).replace('T', ' ');
+    
+    const finalizeResponse = await fetch(`${apiUrl}/upload/finalize`, {
+        method: 'POST',
+        headers: {
+            'X-Api-Key': apiKey,
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+            upload_id: uploadId,
+            total_chunks: totalChunks,
+            original_file_name: file.name,
+            description: description,
+            message: 'Uploaded via JavaScript API example',
+            password: password,
+            reference: reference,
+            active_to: activeTo
+        })
+    });
+    
+    const finalResult = await finalizeResponse.json();
+    if (!finalResult.media_id) {
+        throw new Error('Failed to finalize upload');
+    }
+    
+    return finalResult;
+}
+
+// Example usage with HTML file input
+document.getElementById('file-input').addEventListener('change', async (event) => {
+    const file = event.target.files[0];
+    if (!file) return;
+    
+    const progressBar = document.getElementById('progress-bar');
+    
+    try {
+        const result = await uploadSharedFile(
+            file,
+            'Example document uploaded via JavaScript',
+            'secure_password',
+            'REF123',
+            (progress) => {
+                progressBar.style.width = `${progress}%`;
+            }
+        );
+        
+        console.log('File uploaded successfully!');
+        console.log(`Media ID: ${result.media_id}`);
+        console.log(`Permalink: ${result.permalink}`);
+        console.log(`Shortcode: ${result.shortcode}`);
+    } catch (error) {
+        console.error('Error:', error.message);
+    }
+});
+```
+
+### PHP Example: Get File List
+
+```php
+<?php
+/**
+ * Example of retrieving a list of files
+ */
+function getSharedFiles($page = 1, $perPage = 10, $orderBy = 'upload_date', $order = 'DESC', $fileName = '', $reference = '') {
+    $api_key = 'your-api-key-here';
+    $api_url = 'https://your-wordpress-site.com/wp-json/qdr-tss/v1';
+    
+    // Build query parameters
+    $query_params = http_build_query([
+        'page' => $page,
+        'per_page' => $perPage,
+        'orderby' => $orderBy,
+        'order' => $order,
+        'original_file_name' => $fileName,
+        'reference' => $reference
+    ]);
+    
+    $ch = curl_init($api_url . '/media?' . $query_params);
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+    curl_setopt($ch, CURLOPT_HTTPHEADER, [
+        'X-Api-Key: ' . $api_key
+    ]);
+    
+    $response = curl_exec($ch);
+    $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
+    $header = substr($response, 0, $header_size);
+    $body = substr($response, $header_size);
+    
+    // Get pagination headers
+    $total = 0;
+    $total_pages = 0;
+    
+    foreach (explode("\r\n", $header) as $line) {
+        if (strpos($line, 'X-WP-Total:') === 0) {
+            $total = intval(trim(str_replace('X-WP-Total:', '', $line)));
+        } else if (strpos($line, 'X-WP-TotalPages:') === 0) {
+            $total_pages = intval(trim(str_replace('X-WP-TotalPages:', '', $line)));
+        }
+    }
+    
+    curl_close($ch);
+    
+    $files = json_decode($body, true);
+    
+    return [
+        'files' => $files,
+        'total' => $total,
+        'total_pages' => $total_pages
+    ];
+}
+
+// Example usage
+$result = getSharedFiles(1, 10, 'upload_date', 'DESC', '', 'REF123');
+
+echo "Total files: " . $result['total'] . "\n";
+echo "Total pages: " . $result['total_pages'] . "\n";
+
+foreach ($result['files'] as $file) {
+    echo "ID: " . $file['media_id'] . ", Name: " . $file['original_file_name'] . "\n";
+}
+```
+
+### JavaScript Example: Update File Properties
+
+```javascript
+/**
+ * Updates file properties
+ * @param {string} mediaId The media ID
+ * @param {object} properties Properties to update
+ * @returns {Promise} Promise resolving to the updated file
+ */
+async function updateSharedFile(mediaId, properties) {
+    const apiKey = 'your-api-key-here';
+    const apiUrl = 'https://your-wordpress-site.com/wp-json/qdr-tss/v1';
+    
+    const response = await fetch(`${apiUrl}/media/${mediaId}`, {
+        method: 'PUT',
+        headers: {
+            'X-Api-Key': apiKey,
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify(properties)
+    });
+    
+    const result = await response.json();
+    if (!result.media_id) {
+        throw new Error('Failed to update file');
+    }
+    
+    return result;
+}
+
+// Example usage
+async function exampleUpdateFile() {
+    try {
+        const updatedFile = await updateSharedFile('1621234567', {
+            message: 'Updated message',
+            active_to: '2025-07-19 15:30:00',
+            reference: 'UPDATED-REF'
+        });
+        
+        console.log('File updated successfully!');
+        console.log(updatedFile);
+    } catch (error) {
+        console.error('Error:', error.message);
+    }
+}
+
+exampleUpdateFile();
+```
+
+## Error Handling
+
+All API endpoints return standard HTTP status codes:
+
+- `200 OK`: Request succeeded
+- `201 Created`: Resource created successfully
+- `400 Bad Request`: Invalid request parameters
+- `401 Unauthorized`: Missing API key
+- `403 Forbidden`: Invalid API key
+- `404 Not Found`: Resource not found
+- `500 Internal Server Error`: Server error
+
+Error responses include a message explaining the error:
+
+```json
+{
+  "code": "qdr_tss_not_found",
+  "message": "Media not found.",
+  "data": {
+    "status": 404
+  }
+}
+```
+
+## Rate Limiting
+
+There are currently no rate limits on the API, but excessive usage may be throttled in the future.
+
+## Best Practices
+
+1. Always use chunked uploads for large files
+2. Set reasonable expiration dates for files to avoid cluttering storage
+3. Use unique reference codes to help identify files
+4. Regularly check and purge expired files using the admin interface
+5. Include descriptive file names and messages for better user experience
+