diff --git a/AquaCubeIT.Service.NetFloppy/.dockerignore b/AquaCubeIT.Service.NetFloppy/.dockerignore
new file mode 100644
index 0000000..fe1152b
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/.dockerignore
@@ -0,0 +1,30 @@
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
+!**/.gitignore
+!.git/HEAD
+!.git/config
+!.git/packed-refs
+!.git/refs/heads/**
\ No newline at end of file
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/AquaCubeIT.Service.NetFloppy.DataAccess.csproj b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/AquaCubeIT.Service.NetFloppy.DataAccess.csproj
new file mode 100644
index 0000000..125f4c9
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/AquaCubeIT.Service.NetFloppy.DataAccess.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Class1.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Class1.cs
new file mode 100644
index 0000000..46f63f9
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Class1.cs
@@ -0,0 +1,7 @@
+namespace AquaCubeIT.Service.NetFloppy.DataAccess
+{
+ public class Class1
+ {
+
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Models/MediaBlock.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Models/MediaBlock.cs
new file mode 100644
index 0000000..3693884
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Models/MediaBlock.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace AquaCubeIT.Service.NetFloppy.DataAccess.Models
+{
+ public class MediaBlock
+ {
+ public Guid Id { get; set; }
+ public required MediaImage MediaImage { get; set; }
+ public long BlockId { get; set; }
+ public byte[] Data { get; set; }
+ public DateTime DateStamp { get; set; }
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Models/MediaImage.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Models/MediaImage.cs
new file mode 100644
index 0000000..4712b3e
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Models/MediaImage.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace AquaCubeIT.Service.NetFloppy.DataAccess.Models
+{
+ public class MediaImage
+ {
+ public Guid Id { get; set; }
+ public required string Name { get; set; }
+ public string? Description { get; set; }
+ public required MediaType MediaType { get; set; }
+ public long SizeInBytes { get; set; }
+ public int Hash { get; set; }
+ public long NoOfBlocks { get; set; }
+ public long BlockSize { get; set; }
+ public DateTime DateStamp { get; set; }
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Models/MediaType.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Models/MediaType.cs
new file mode 100644
index 0000000..2c3e7df
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Models/MediaType.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace AquaCubeIT.Service.NetFloppy.DataAccess.Models
+{
+ ///
+ /// Media type of the stored data image
+ ///
+ public class MediaType
+ {
+ public Guid Id { get; set; }
+ public required string Name { get; set; }
+ public string? Description { get; set; }
+ public DateTime DateStamp { get; set; }
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.sln b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.sln
new file mode 100644
index 0000000..7ad24c8
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.13.35828.75
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaCubeIT.Service.NetFloppy", "AquaCubeIT.Service.NetFloppy\AquaCubeIT.Service.NetFloppy.csproj", "{BBCB4FB7-219C-49D4-9F20-C65189D820BD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaCubeIT.Service.NetFloppy.DataAccess", "AquaCubeIT.Service.NetFloppy.DataAccess\AquaCubeIT.Service.NetFloppy.DataAccess.csproj", "{883D7BF4-EC7F-436F-AE3A-5BDE9E024801}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {BBCB4FB7-219C-49D4-9F20-C65189D820BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BBCB4FB7-219C-49D4-9F20-C65189D820BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BBCB4FB7-219C-49D4-9F20-C65189D820BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BBCB4FB7-219C-49D4-9F20-C65189D820BD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {883D7BF4-EC7F-436F-AE3A-5BDE9E024801}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {883D7BF4-EC7F-436F-AE3A-5BDE9E024801}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {883D7BF4-EC7F-436F-AE3A-5BDE9E024801}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {883D7BF4-EC7F-436F-AE3A-5BDE9E024801}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {BE3474A5-542F-4060-AE03-C0FB9EBA0A0E}
+ EndGlobalSection
+EndGlobal
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.csproj b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.csproj
new file mode 100644
index 0000000..aae4f4f
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net9.0
+ enable
+ enable
+ f3d90137-247e-42ea-b274-3bbba8be89c4
+ Linux
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.csproj.user b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.csproj.user
new file mode 100644
index 0000000..983ecfc
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.csproj.user
@@ -0,0 +1,9 @@
+
+
+
+ http
+
+
+ ProjectDebugger
+
+
\ No newline at end of file
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.http b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.http
new file mode 100644
index 0000000..c855c4d
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.http
@@ -0,0 +1,9 @@
+@AquaCubeIT.Service.NetFloppy_HostAddress = http://localhost:9000
+
+GET {{AquaCubeIT.Service.NetFloppy_HostAddress}}/register/
+Accept: application/json
+
+GET {{AquaCubeIT.Service.NetFloppy_HostAddress}}/logon/
+Accept: application/json
+
+###
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Controllers/LogonController.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Controllers/LogonController.cs
new file mode 100644
index 0000000..d75a939
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Controllers/LogonController.cs
@@ -0,0 +1,177 @@
+using AquaCubeIT.Service.NetFloppy.Models;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Identity.Data;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.IdentityModel.Tokens;
+using System.IdentityModel.Tokens.Jwt;
+using System.Net.Sockets;
+using System.Security.Claims;
+using System.Security.Cryptography;
+using System.Text;
+using RefreshRequest = AquaCubeIT.Service.NetFloppy.Models.RefreshRequest;
+
+namespace AquaCubeIT.Service.NetFloppy.Controllers;
+
+[ApiController]
+[Route("[controller]")]
+public class LogonController : ControllerBase
+{
+ private readonly IConfiguration _config;
+ private readonly ILogger _logger;
+
+ public LogonController(ILogger logger, IConfiguration config)
+ {
+ _config = config;
+ _logger = logger;
+ }
+
+ [HttpGet]
+ [Authorize]
+ public async Task Get()
+ {
+ var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
+ var email = User.FindFirstValue(ClaimTypes.Email);
+ var roles = User.FindAll(ClaimTypes.Role).Select(c => c.Value).ToList();
+
+ var allClaims = User.Claims.Select(c => c.Value).ToList();
+
+ return $"User: {userId}, {email}, {string.Join(",", roles)}, {string.Join(",",allClaims)}";
+ }
+
+ private RefreshResponsePayload GenerateToken(RefreshRequestPayload request)
+ {
+
+ if (request.RefreshToken == null)
+ {
+ throw new BadHttpRequestException("Bad Payload");
+ }
+
+ // Hash incoming token the same way
+ byte[] raw = Convert.FromBase64String(request.RefreshToken);
+ string reqHash = Convert.ToHexString(SHA256.HashData(raw));
+
+ // Get token by hash
+
+
+
+ // Check family is ok
+
+ string? family = null;
+ var userName = "Test";
+
+ // Check if revoked
+
+
+
+ // Throw error if revoked or expired kill all family tokens
+
+
+
+ // Save refresh token by hash
+
+ // Check
+ return GenerateToken(userName, GenerateRefreshToken(family));
+ }
+
+ private string GenerateRefreshToken(string? family)
+ {
+ // Update refreshToken
+ var bytes = RandomNumberGenerator.GetBytes(64);
+ var refreshToken = Convert.ToBase64String(bytes);
+ var hash = Convert.ToHexString(SHA256.HashData(bytes));
+ family = family ?? Guid.NewGuid().ToString("N");
+
+ // Save refresh token by hash
+
+ return refreshToken;
+ }
+
+ private LogonResponsePayload GenerateToken(LogonRequestPayload request)
+ {
+ return GenerateToken(request.UserName, GenerateRefreshToken(null));
+ }
+
+ private RefreshResponsePayload GenerateToken(string? userName, string refreshToken)
+ {
+ var claims = new[]
+ {
+ new Claim(ClaimTypes.NameIdentifier, userName),
+ new Claim(ClaimTypes.Name, "Stephen Barriball"),
+ new Claim(ClaimTypes.Role, "User")
+ };
+
+ var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
+ var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
+ var expiryDate = DateTime.Now.AddMinutes(15);
+ var token = new JwtSecurityToken(
+ issuer: _config["Jwt:Issuer"],
+ audience: _config["Jwt:Audience"],
+ claims: claims,
+ expires: expiryDate,
+ signingCredentials: creds);
+
+ // Return new token and refresh token
+ return new RefreshResponsePayload() { Token = new JwtSecurityTokenHandler().WriteToken(token), RefreshToken = refreshToken, TokenExpiry = expiryDate };
+ }
+
+
+ [HttpPost]
+ public async Task Post(LogonRequest request)
+ {
+ var response = new LogonResponse();
+
+ if (request.Payload == null)
+ {
+ throw new Exception("Invalid Payload");
+ }
+
+ // Validate user here
+ if (request.Payload.UserName == "test" && request.Payload.Password == "password")
+ {
+ response.Payload = GenerateToken(request.Payload);
+ response.Success = true;
+ }
+ else
+ {
+ response.Success = false;
+ response.Error = new ErrorResponsePayload() { ErrorNo = 15, Message = "Invalid Login" };
+ }
+
+ return response;
+ }
+
+ [HttpPost("refresh")]
+ public async Task Refresh(RefreshRequest request)
+ {
+ try
+ {
+ if (request.Payload == null)
+ {
+ throw new BadHttpRequestException("Invalid Payload");
+ }
+
+ // Hash incoming token the same way
+ byte[] raw = Convert.FromBase64String(request.Payload.RefreshToken);
+ string reqHash = Convert.ToHexString(SHA256.HashData(raw));
+
+ //var stored = await _repo.GetRefreshTokenByHashAsync(reqHash);
+
+ //if (stored is null || stored.RevokedAtUtc != null || stored.ExpiresAtUtc < DateTime.UtcNow)
+ //{
+ // throw new UnauthorizedAccessException();
+ //return Unauthorized();
+ //}
+
+ // Rotate: revoke old, issue new pair in same family
+ //stored.RevokedAtUtc = DateTime.UtcNow;
+
+ //var user = await _users.GetByIdAsync(stored.UserId);
+
+ return new RefreshResponse() { Payload = GenerateToken(request.Payload), Success = true };
+ }
+ catch(Exception e)
+ {
+ return new RefreshResponse() { Success = false, Error = new ErrorResponsePayload() { Exception = e, Message = e.Message, ErrorNo = 14 } };
+ }
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Controllers/OperationsController.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Controllers/OperationsController.cs
new file mode 100644
index 0000000..d1edbfd
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Controllers/OperationsController.cs
@@ -0,0 +1,174 @@
+using AquaCubeIT.Service.NetFloppy.Models;
+using Fat16Lib;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
+using System;
+using System.IO;
+using System.Text;
+
+namespace AquaCubeIT.Service.NetFloppy.Controllers;
+
+[ApiController]
+[Route("[controller]")]
+public class OperationsController : ControllerBase
+{
+ private readonly ILogger _logger;
+ private const int DISK_SIZE_MB = 5;
+ private const int DISK_SECTOR_SIZE = 512; // 512 bytes/sector
+ private const int DISK_SECTOR_COUNT = DISK_SIZE_MB * 1024 * 1024 / DISK_SECTOR_SIZE; // ~2 MB (4000 * 512)
+ private const long DISK_BYTE_SIZE = DISK_SECTOR_COUNT * DISK_SECTOR_SIZE;
+
+ public OperationsController(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ private ErrorResponsePayload GetExceptionPayload(Exception exception)
+ {
+ return new ErrorResponsePayload() { ErrorNo = 16, Message = exception.Message, Exception = exception };
+ }
+
+ [Authorize(Policy = "MustBeUser")]
+ [HttpPost("LoadImage")]
+ public async Task LoadImage(LoadImageRequest request)
+ {
+ try
+ {
+ // Check image exists
+
+
+ // Check permissions of user
+
+
+
+ // Get image data
+ byte[] data = null;
+
+ if (System.IO.File.Exists("c:\\temp\\test_image1.img"))
+ {
+
+ data = await System.IO.File.ReadAllBytesAsync("c:\\temp\\test_image1.img");
+ }
+ else
+ {
+ var img = new Fat16Image(new Fat16ImageOptions
+ {
+ TotalSizeBytes = DISK_BYTE_SIZE,
+ VolumeLabel = "NetFloppy"
+ });
+
+ img.AddFile("README.TXT", Encoding.ASCII.GetBytes("Welcome to NetFloppy\r\n"));
+ img.AddFile("FF.CFG", Encoding.ASCII.GetBytes("# FF.CFG - FlashFloppy configuration (served inside FAT by backend)\r\ninterface = ibmpc\r\nhost = acorn\r\nindex-suppression = no\r\n\r\n# Enable speaker / fake disk drive sounds\r\nsound = on\r\nsound-volume = 19\r\n\r\n# Disable display devices\r\ndisplay-type = none\r\n"));
+ img.AddFile("TEST.ADF", System.IO.File.ReadAllBytes("TestDisk.adf"));
+
+ data = img.GetImage();
+ img.SaveToFile("c:\\temp\\test_image1.img");
+
+ }
+
+
+ return new LoadImageResponse() { Success = true, Payload = new LoadImageResponsePayload() { DiskImage = data } };
+ }
+ catch(Exception e)
+ {
+ return new LoadImageResponse() { Success = false, Error = GetExceptionPayload(e) };
+ }
+ }
+
+ private bool InRange(int lba, int offset, int len)
+ {
+
+ long start = lba * DISK_SECTOR_SIZE + offset;
+ long end = start + len;
+ return end <= DISK_BYTE_SIZE;
+ }
+
+ //[Authorize(Policy = "MustBeUser")]
+ [HttpPost("LoadSector")]
+ public async Task LoadSector(LoadSectorRequest request)
+ {
+ try
+ {
+ // Check image exists
+
+
+ // Check permissions of user
+
+
+
+ // Get image data
+ byte[] data = null;
+
+ if (System.IO.File.Exists("c:\\temp\\test_image1.img"))
+ {
+
+ data = await System.IO.File.ReadAllBytesAsync("c:\\temp\\test_image1.img");
+ }
+ else
+ {
+ var img = new Fat16Image(new Fat16ImageOptions
+ {
+ TotalSizeBytes = DISK_BYTE_SIZE,
+ VolumeLabel = "NetFloppy"
+ });
+
+ img.AddFile("README.TXT", Encoding.ASCII.GetBytes("Welcome to NetFloppy\r\n"));
+ img.AddFile("FF.CFG", Encoding.ASCII.GetBytes("# FF.CFG - FlashFloppy configuration (served inside FAT by backend)\r\ninterface = ibmpc\r\nhost = acorn\r\nindex-suppression = no\r\n\r\n# Enable speaker / fake disk drive sounds\r\nsound = on\r\nsound-volume = 19\r\n\r\n# Disable display devices\r\ndisplay-type = none\r\n"));
+ img.AddFile("TEST.ADF", System.IO.File.ReadAllBytes("TestDisk.adf"));
+
+ data = img.GetImage();
+ //img.SaveToFile("c:\\temp\\test_image1.img");
+
+ }
+
+ if (!this.InRange(request.Payload.Lba, request.Payload.Offset, request.Payload.Length))
+ {
+ throw new Exception("Invalid Disk Location");
+ }
+
+ var startPosition = (request.Payload.Lba * DISK_SECTOR_SIZE) + request.Payload.Offset;
+ var sectorData = data.Skip(startPosition).Take(request.Payload.Length).ToArray();
+
+ //Response.ContentType = "application/octet-stream";
+ //Response.ContentLength = sectorData.Length;
+ //await Response.Body.WriteAsync(sectorData,0 , sectorData.Length);
+
+ return new LoadSectorResponse() { Success = true, Payload = new LoadSectorResponsePayload() { Data = sectorData, Length = sectorData.Length } };
+
+ //return new EmptyResult();
+ }
+ catch (Exception e)
+ {
+ //return new LoadSectorResponse() { Success = false, Error = GetExceptionPayload(e) };
+ return BadRequest(e.Message);
+ }
+ }
+
+ [Authorize(Policy = "MustBeUser")]
+ [HttpPost("SaveImage")]
+ public async Task SaveImage(SaveImageRequest request)
+ {
+ try
+ {
+ // Check image exists
+
+
+ // Check permissions of user
+
+
+
+ // Get image data
+ var data = request.Payload.Data;
+
+ // Save to file
+ await System.IO.File.WriteAllBytesAsync("c:\\temp\\test_image1.img", data);
+
+ return new SaveImageResponse() { Success = true, Payload = new SaveImageResponsePayload() { BytesSaved = data.Length } };
+ }
+ catch (Exception e)
+ {
+ return new SaveImageResponse() { Success = false, Error = GetExceptionPayload(e) };
+ }
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Controllers/RegisterController.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Controllers/RegisterController.cs
new file mode 100644
index 0000000..d4c607b
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Controllers/RegisterController.cs
@@ -0,0 +1,30 @@
+using AquaCubeIT.Service.NetFloppy.Models;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace AquaCubeIT.Service.NetFloppy.Controllers;
+
+[ApiController]
+[Route("[controller]")]
+public class RegisterController : ControllerBase
+{
+ private readonly ILogger _logger;
+
+ public RegisterController(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ [Authorize(Policy = "MustBeUser")]
+ [HttpPost]
+ public RegisterResponse Post(RegisterRequest request)
+ {
+ return new RegisterResponse() { Success = true, Payload = new RegisterResponsePayload() { Name = "Test", Description = "Test Drive", EmulationType = "Unknown Floppy Type" }};
+ }
+
+ [HttpGet]
+ public RegisterResponse Get()
+ {
+ return new RegisterResponse() { Success = true, Payload = new RegisterResponsePayload() { Name = "Test", Description = "Test Drive", EmulationType = "Unknown Floppy Type" } };
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Dockerfile b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Dockerfile
new file mode 100644
index 0000000..a8132f0
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Dockerfile
@@ -0,0 +1,30 @@
+# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
+
+# This stage is used when running from VS in fast mode (Default for Debug configuration)
+FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
+USER $APP_UID
+WORKDIR /app
+EXPOSE 8080
+EXPOSE 8081
+
+
+# This stage is used to build the service project
+FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.csproj", "AquaCubeIT.Service.NetFloppy/"]
+RUN dotnet restore "./AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.csproj"
+COPY . .
+WORKDIR "/src/AquaCubeIT.Service.NetFloppy"
+RUN dotnet build "./AquaCubeIT.Service.NetFloppy.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+# This stage is used to publish the service project to be copied to the final stage
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./AquaCubeIT.Service.NetFloppy.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "AquaCubeIT.Service.NetFloppy.dll"]
\ No newline at end of file
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Helpers/Fat12Lib.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Helpers/Fat12Lib.cs
new file mode 100644
index 0000000..2372a69
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Helpers/Fat12Lib.cs
@@ -0,0 +1,341 @@
+// Fat12ImageLib.cs
+// Minimal C# library for creating a FAT12 disk image and adding files (root directory only)
+// Target: .NET 6+
+// Limitations: 8.3 filenames, root directory only (no subdirectories), no long file names, no deletions/compaction.
+
+using System;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace Fat12Lib
+{
+ public class Fat12ImageOptions
+ {
+ // Total size of the image in bytes. Common sizes: 360 KiB, 720 KiB, 1.2 MiB, 1.44 MiB, 2.88 MiB, etc.
+ public long TotalSizeBytes { get; set; } = 1440L * 1024; // 1.44 MiB default
+ public ushort BytesPerSector { get; set; } = 512;
+ public byte SectorsPerCluster { get; set; } = 0; // 0 = auto-select based on size
+ public byte NumFats { get; set; } = 2;
+ public ushort ReservedSectors { get; set; } = 1; // boot sector
+ public ushort MaxRootDirEntries { get; set; } = 224; // typical for 1.44MB floppy
+ public byte MediaDescriptor { get; set; } = 0xF0; // floppy
+ public ushort SectorsPerTrack { get; set; } = 18; // 1.44MB geometry (not critical)
+ public ushort NumberOfHeads { get; set; } = 2; // geometry (not critical)
+ public uint HiddenSectors { get; set; } = 0;
+ public string OemId { get; set; } = "MSDOS5.0";
+ public string VolumeLabel { get; set; } = "NO NAME "; // exactly 11 chars; will be padded/trimmed
+ public string FsTypeLabel { get; set; } = "FAT12 "; // 8 chars in boot sector label
+ public ushort? VolumeId { get; set; } = null; // null => random
+ }
+
+ public class Fat12Image
+ {
+ private readonly Fat12ImageOptions _opt;
+
+ // Computed layout
+ private uint _totalSectors; // 16-bit or 32-bit depending on size
+ private ushort _rootDirSectors;
+ private ushort _fatSectors; // sectors per FAT
+ private uint _firstFatSectorLba;
+ private uint _firstRootDirSectorLba;
+ private uint _firstDataSectorLba;
+ private uint _clusterCount; // data clusters (not counting 0 and 1)
+ private byte _sectorsPerCluster;
+
+ // Runtime buffers
+ private byte[] _bootSector = Array.Empty();
+ private byte[] _fat; // a single FAT table; will be duplicated NumFats times
+ private byte[] _rootDir; // root directory area (fixed size)
+ private byte[] _data; // data region sized to contain all clusters (clusterCount * spc * bps)
+
+ // Allocation state
+ private uint _nextFreeCluster = 2; // FAT clusters start at 2
+ private int _nextFreeRootDirEntry = 0;
+
+ public Fat12Image(Fat12ImageOptions options)
+ {
+ _opt = options;
+ if (_opt.BytesPerSector != 512)
+ throw new NotSupportedException("Only 512-byte sectors supported in this minimal implementation.");
+ if (_opt.TotalSizeBytes % _opt.BytesPerSector != 0)
+ throw new ArgumentException("TotalSizeBytes must be a multiple of BytesPerSector.");
+
+ InitializeLayout();
+ BuildBootSector();
+ InitializeFatAndRoot();
+ }
+
+ // Adds a file into the root directory. Uses 8.3 name rules.
+ public void AddFile(string fileName, byte[] content, DateTime? timestamp = null)
+ {
+ if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentException("fileName");
+ if (content == null) throw new ArgumentNullException(nameof(content));
+
+ var (name83, ext83) = ToShortName(fileName);
+ if (_nextFreeRootDirEntry >= _opt.MaxRootDirEntries)
+ throw new IOException("Root directory is full.");
+
+ // Compute how many clusters needed
+ uint bytesPerCluster = (uint)(_opt.BytesPerSector * _sectorsPerCluster);
+ uint clustersNeeded = (uint)((content.Length + bytesPerCluster - 1) / bytesPerCluster);
+ if (clustersNeeded == 0) clustersNeeded = 1; // allocate 1 cluster to store 0-byte file metadata/simple handling
+
+ if ((_nextFreeCluster - 2) + clustersNeeded > _clusterCount)
+ throw new IOException("Not enough free space for file.");
+
+ // Allocate cluster chain
+ List chain = new();
+ for (int i = 0; i < clustersNeeded; i++)
+ {
+ uint c = _nextFreeCluster++;
+ chain.Add(c);
+ }
+
+ // Write data to clusters
+ int srcOffset = 0;
+ foreach (var c in chain)
+ {
+ uint lba = ClusterToLba(c);
+ uint offset = (lba - _firstDataSectorLba) * _opt.BytesPerSector;
+ int toCopy = (int)Math.Min(bytesPerCluster, (uint)(content.Length - srcOffset));
+ if (toCopy > 0)
+ Buffer.BlockCopy(content, srcOffset, _data, (int)offset, toCopy);
+ srcOffset += toCopy;
+ }
+
+ // Link FAT entries (12-bit values)
+ for (int i = 0; i < chain.Count - 1; i++)
+ SetFat12Entry(chain[i], (ushort)chain[i + 1]);
+ SetFat12Entry(chain[^1], 0xFFF); // EOF for FAT12
+
+ // Build directory entry
+ var dt = timestamp ?? DateTime.Now;
+ ushort time = ToFatTime(dt);
+ ushort date = ToFatDate(dt);
+
+ Span entry = stackalloc byte[32];
+ entry.Clear();
+ Encoding.ASCII.GetBytes(name83, entry);
+ Encoding.ASCII.GetBytes(ext83, entry.Slice(8));
+ entry[11] = 0x20; // Archive
+ BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(14), time); // Create time
+ BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(16), date); // Create date
+ BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(18), date); // Last access date (approx)
+ BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(22), time); // Write time
+ BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(24), date); // Write date
+ BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(26), (ushort)chain[0]); // First cluster
+ BinaryPrimitives.WriteUInt32LittleEndian(entry.Slice(28), (uint)content.Length); // File size
+
+ // Write entry into root dir
+ int rootOffset = _nextFreeRootDirEntry * 32;
+ _rootDir.AsSpan(rootOffset, 32).Clear();
+ entry.CopyTo(_rootDir.AsSpan(rootOffset));
+ _nextFreeRootDirEntry++;
+ }
+
+ public byte[] GetImage()
+ {
+ using var ms = new MemoryStream((int)(_opt.TotalSizeBytes));
+
+ // Boot sector
+ ms.Write(_bootSector, 0, _bootSector.Length);
+
+ // FATs
+ for (int f = 0; f < _opt.NumFats; f++)
+ ms.Write(_fat, 0, _fat.Length);
+
+ // Root directory
+ ms.Write(_rootDir, 0, _rootDir.Length);
+
+ // Data region
+ ms.Write(_data, 0, _data.Length);
+
+ // Pad to exact size if necessary (should be exact)
+ if (ms.Length < _opt.TotalSizeBytes)
+ ms.Write(new byte[_opt.TotalSizeBytes - ms.Length], 0, (int)(_opt.TotalSizeBytes - ms.Length));
+
+ return ms.ToArray();
+ }
+
+ public void SaveToFile(string path)
+ {
+ File.WriteAllBytes(path, GetImage());
+ }
+
+ private void InitializeLayout()
+ {
+ _totalSectors = (uint)(_opt.TotalSizeBytes / _opt.BytesPerSector);
+
+ // Decide sectors per cluster if not specified
+ _sectorsPerCluster = _opt.SectorsPerCluster != 0 ? _opt.SectorsPerCluster : SelectSectorsPerCluster(_totalSectors, _opt.BytesPerSector);
+
+ _rootDirSectors = (ushort)(((uint)_opt.MaxRootDirEntries * 32 + (_opt.BytesPerSector - 1)) / _opt.BytesPerSector);
+
+ // Iteratively solve for FAT size and cluster count (FAT12 uses 12-bit entries => 1.5 bytes per entry)
+ ushort fatSectors = 1;
+ while (true)
+ {
+ uint dataSectors = _totalSectors - _opt.ReservedSectors - (uint)(_opt.NumFats * fatSectors) - _rootDirSectors;
+ uint clusterCount = dataSectors / _sectorsPerCluster;
+ if (clusterCount < 1 || clusterCount > 4084)
+ throw new NotSupportedException($"Cluster count {clusterCount} out of FAT12 bounds (needs 1..4,084). Adjust size or sectors/cluster.");
+
+ // FAT12 entries: (clusterCount + 2) entries, each 12 bits => total bytes = ceil(3 * entries / 2)
+ uint entries = clusterCount + 2;
+ uint fatBytes = (entries * 3 + 1) / 2; // ceil(1.5 * entries)
+ ushort requiredFatSectors = (ushort)((fatBytes + (_opt.BytesPerSector - 1)) / _opt.BytesPerSector);
+ if (requiredFatSectors == fatSectors)
+ {
+ _fatSectors = fatSectors;
+ _clusterCount = clusterCount;
+ break;
+ }
+ fatSectors = requiredFatSectors;
+ }
+
+ _firstFatSectorLba = _opt.ReservedSectors;
+ _firstRootDirSectorLba = _firstFatSectorLba + (uint)(_opt.NumFats * _fatSectors);
+ _firstDataSectorLba = _firstRootDirSectorLba + _rootDirSectors;
+
+ // Buffers
+ _bootSector = new byte[_opt.BytesPerSector];
+ _fat = new byte[_fatSectors * _opt.BytesPerSector];
+ _rootDir = new byte[_rootDirSectors * _opt.BytesPerSector];
+ uint dataSectorsFinal = _totalSectors - _opt.ReservedSectors - (uint)(_opt.NumFats * _fatSectors) - _rootDirSectors;
+ _data = new byte[dataSectorsFinal * _opt.BytesPerSector];
+ }
+
+ private void BuildBootSector()
+ {
+ Span bs = _bootSector;
+ bs.Clear();
+
+ // Jump + OEM
+ bs[0] = 0xEB; bs[1] = 0x3C; bs[2] = 0x90; // JMP short
+ Encoding.ASCII.GetBytes((_opt.OemId ?? "MSDOS5.0").PadRight(8).Substring(0,8), bs.Slice(3, 8));
+
+ // BIOS Parameter Block (BPB)
+ BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(11), _opt.BytesPerSector);
+ bs[13] = _sectorsPerCluster;
+ BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(14), _opt.ReservedSectors);
+ bs[16] = _opt.NumFats;
+ BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(17), _opt.MaxRootDirEntries);
+ ushort totalSectors16 = _totalSectors <= 0xFFFF ? (ushort)_totalSectors : (ushort)0;
+ BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(19), totalSectors16);
+ bs[21] = _opt.MediaDescriptor;
+ BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(22), _fatSectors);
+ BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(24), _opt.SectorsPerTrack);
+ BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(26), _opt.NumberOfHeads);
+ BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(28), _opt.HiddenSectors);
+ BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(32), _totalSectors);
+
+ // Extended BPB for FAT12/FAT16
+ ushort volId = _opt.VolumeId ?? (ushort)Random.Shared.Next(1, 0xFFFF);
+ BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(39), volId);
+ string label = (_opt.VolumeLabel ?? "NO NAME").ToUpperInvariant();
+ label = new string(label.Where(ch => ch >= 0x20 && ch <= 0x7E).ToArray());
+ label = label.PadRight(11).Substring(0, 11);
+ Encoding.ASCII.GetBytes(label, bs.Slice(43, 11));
+ string fstype = (_opt.FsTypeLabel ?? "FAT12").PadRight(8).Substring(0, 8);
+ Encoding.ASCII.GetBytes(fstype, bs.Slice(54, 8));
+
+ // Minimal bootstrap code area (ignored for data disks)
+ bs[_opt.BytesPerSector - 2] = 0x55; // signature
+ bs[_opt.BytesPerSector - 1] = 0xAA;
+ }
+
+ private void InitializeFatAndRoot()
+ {
+ // FAT initial entries for FAT12
+ // First three bytes: media descriptor and end markers -> usually F0 FF FF for 0xF0 media
+ Array.Clear(_fat, 0, _fat.Length);
+ _fat[0] = _opt.MediaDescriptor; // e.g., 0xF0
+ _fat[1] = 0xFF;
+ _fat[2] = 0xFF; // cluster 1 reserved/end
+ }
+
+ private uint ClusterToLba(uint cluster)
+ {
+ if (cluster < 2) throw new ArgumentOutOfRangeException(nameof(cluster));
+ return _firstDataSectorLba + (cluster - 2) * _sectorsPerCluster;
+ }
+
+ private void SetFat12Entry(uint cluster, ushort value)
+ {
+ if (cluster >= _clusterCount + 2) throw new ArgumentOutOfRangeException(nameof(cluster));
+ // value is 12-bit
+ value &= 0x0FFF;
+ int index = (int)(cluster + (cluster / 2)); // floor(1.5 * cluster)
+ if ((cluster & 1) == 0)
+ {
+ // even cluster: low 12 bits starting at index
+ _fat[index] = (byte)((_fat[index] & 0x00) | (value & 0xFF));
+ _fat[index + 1] = (byte)((_fat[index + 1] & 0xF0) | ((value >> 8) & 0x0F));
+ }
+ else
+ {
+ // odd cluster: high 12 bits overlapping starting at index
+ _fat[index] = (byte)((_fat[index] & 0x0F) | ((value << 4) & 0xF0));
+ _fat[index + 1] = (byte)((_fat[index + 1] & 0x00) | ((value >> 4) & 0xFF));
+ }
+ }
+
+ private static (string name, string ext) ToShortName(string fileName)
+ {
+ // Extract base and extension, sanitize to 8.3 uppercase ASCII
+ string name = Path.GetFileNameWithoutExtension(fileName) ?? "FILE";
+ string ext = Path.GetExtension(fileName);
+ if (ext.StartsWith('.')) ext = ext.Substring(1);
+
+ string Sanitize(string s) => new string(s.ToUpperInvariant()
+ .Select(c => (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || "!#$%&'()-@^_`{}~".Contains(c) ? c : '_')
+ .ToArray());
+
+ string n = Sanitize(name);
+ string e = Sanitize(ext);
+ if (n.Length > 8) n = n.Substring(0, 8);
+ if (e.Length > 3) e = e.Substring(0, 3);
+ n = n.PadRight(8);
+ e = e.PadRight(3);
+ return (n, e);
+ }
+
+ private static ushort ToFatDate(DateTime dt)
+ {
+ int year = Math.Clamp(dt.Year - 1980, 0, 127);
+ int month = Math.Clamp(dt.Month, 1, 12);
+ int day = Math.Clamp(dt.Day, 1, 31);
+ return (ushort)((year << 9) | (month << 5) | day);
+ }
+
+ private static ushort ToFatTime(DateTime dt)
+ {
+ int hour = Math.Clamp(dt.Hour, 0, 23);
+ int minute = Math.Clamp(dt.Minute, 0, 59);
+ int second = Math.Clamp(dt.Second / 2, 0, 29); // 2-second resolution
+ return (ushort)((hour << 11) | (minute << 5) | second);
+ }
+
+ private static byte SelectSectorsPerCluster(uint totalSectors, ushort bytesPerSector)
+ {
+ // Simple mapping aimed at floppy-like sizes with 512B sectors
+ long sizeKiB = (long)totalSectors * bytesPerSector / 1024;
+ if (sizeKiB <= 720) return 1; // 360K/720K
+ if (sizeKiB <= 1440) return 1; // 1.44MB
+ if (sizeKiB <= 2400) return 1; // 2.4MB
+ if (sizeKiB <= 2880) return 1; // 2.88MB
+ if (sizeKiB <= 4096) return 2; // up to 4MB
+ if (sizeKiB <= 8192) return 4; // up to 8MB
+ return 8; // larger volumes still FAT12 but uncommon
+ }
+ }
+}
+
+// --------- Example usage (not part of the library) ---------
+// using System.Text;
+// var img = new Fat12Lib.Fat12Image(new Fat12Lib.Fat12ImageOptions { TotalSizeBytes = 1440L * 1024, VolumeLabel = "MY12DISK" });
+// img.AddFile("HELLO.TXT", Encoding.ASCII.GetBytes("Hello FAT12!"));
+// img.SaveToFile("fat12.img");
diff --git a/AquaCubeIT.Service.NetFloppy/Fat16Disk.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Helpers/Fat16Disk.cs
similarity index 100%
rename from AquaCubeIT.Service.NetFloppy/Fat16Disk.cs
rename to AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Helpers/Fat16Disk.cs
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/Error.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/Error.cs
new file mode 100644
index 0000000..e176835
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/Error.cs
@@ -0,0 +1,10 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class Error
+ {
+ public long ErrorNo { get; set; }
+ public string Message { get; set; }
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/ErrorResponsePayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/ErrorResponsePayload.cs
new file mode 100644
index 0000000..2f2eb9a
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/ErrorResponsePayload.cs
@@ -0,0 +1,11 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class ErrorResponsePayload
+ {
+ public long ErrorNo { get; set; }
+ public string? Message { get; set; } = null;
+ public Exception? Exception { get; set; }
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageRequest.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageRequest.cs
new file mode 100644
index 0000000..55fafeb
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageRequest.cs
@@ -0,0 +1,9 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class LoadImageRequest : Request
+ {
+
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageRequestPayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageRequestPayload.cs
new file mode 100644
index 0000000..795f782
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageRequestPayload.cs
@@ -0,0 +1,10 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class LoadImageRequestPayload
+ {
+ public string UnitId { get; set; } = string.Empty;
+ public string DiskId { get; set; } = string.Empty;
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageResponse.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageResponse.cs
new file mode 100644
index 0000000..e924981
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageResponse.cs
@@ -0,0 +1,9 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class LoadImageResponse : Response
+ {
+
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageResponsePayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageResponsePayload.cs
new file mode 100644
index 0000000..fc07d90
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageResponsePayload.cs
@@ -0,0 +1,9 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class LoadImageResponsePayload
+ {
+ public byte[] DiskImage { get; set; }
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorRequest.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorRequest.cs
new file mode 100644
index 0000000..1b7399d
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorRequest.cs
@@ -0,0 +1,9 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class LoadSectorRequest : Request
+ {
+
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorRequestPayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorRequestPayload.cs
new file mode 100644
index 0000000..d57b63e
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorRequestPayload.cs
@@ -0,0 +1,13 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class LoadSectorRequestPayload
+ {
+ public string UnitId { get; set; } = string.Empty;
+ public string DiskId { get; set; } = string.Empty;
+ public int Lba { get; set; } = 0;
+ public int Offset { get; set; } = 0;
+ public int Length { get; set; } = 0;
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorResponse.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorResponse.cs
new file mode 100644
index 0000000..debab07
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorResponse.cs
@@ -0,0 +1,9 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class LoadSectorResponse : Response
+ {
+
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorResponsePayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorResponsePayload.cs
new file mode 100644
index 0000000..57a6e73
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorResponsePayload.cs
@@ -0,0 +1,10 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class LoadSectorResponsePayload
+ {
+ public byte[] Data { get; set; }
+ public int Length { get; set; }
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonRequest.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonRequest.cs
new file mode 100644
index 0000000..11c9c8e
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonRequest.cs
@@ -0,0 +1,9 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class LogonRequest : Request
+ {
+
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonRequestPayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonRequestPayload.cs
new file mode 100644
index 0000000..5ddc465
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonRequestPayload.cs
@@ -0,0 +1,10 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class LogonRequestPayload
+ {
+ public string UserName { get; set; } = string.Empty;
+ public string Password { get; set; } = string.Empty;
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonResponse.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonResponse.cs
new file mode 100644
index 0000000..7f87ecf
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonResponse.cs
@@ -0,0 +1,9 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class LogonResponse : Response
+ {
+
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonResponsePayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonResponsePayload.cs
new file mode 100644
index 0000000..2d99219
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonResponsePayload.cs
@@ -0,0 +1,11 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class LogonResponsePayload
+ {
+ public required string Token { get; set; }
+ public required DateTime TokenExpiry { get; set; }
+ public string RefreshToken { get; set; } = string.Empty;
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshRequest.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshRequest.cs
new file mode 100644
index 0000000..ad65566
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshRequest.cs
@@ -0,0 +1,9 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class RefreshRequest : Request
+ {
+
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshRequestPayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshRequestPayload.cs
new file mode 100644
index 0000000..4bf65b0
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshRequestPayload.cs
@@ -0,0 +1,9 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class RefreshRequestPayload
+ {
+ public string RefreshToken { get; set; } = string.Empty;
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshResponse.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshResponse.cs
new file mode 100644
index 0000000..458b3d6
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshResponse.cs
@@ -0,0 +1,9 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class RefreshResponse : Response
+ {
+
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshResponsePayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshResponsePayload.cs
new file mode 100644
index 0000000..7c929b9
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshResponsePayload.cs
@@ -0,0 +1,9 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class RefreshResponsePayload : LogonResponsePayload
+ {
+ public string RefreshToken { get; set; } = string.Empty;
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterReponse.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterReponse.cs
new file mode 100644
index 0000000..fdad9c0
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterReponse.cs
@@ -0,0 +1,9 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class RegisterResponse : Response
+ {
+
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterRequest.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterRequest.cs
new file mode 100644
index 0000000..879e8ab
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterRequest.cs
@@ -0,0 +1,9 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class RegisterRequest : Request
+ {
+
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterRequestPayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterRequestPayload.cs
new file mode 100644
index 0000000..bb51f55
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterRequestPayload.cs
@@ -0,0 +1,9 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class RegisterRequestPayload
+ {
+ public string UnitId { get; set; }
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterResponsePayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterResponsePayload.cs
new file mode 100644
index 0000000..4370ad0
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterResponsePayload.cs
@@ -0,0 +1,12 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class RegisterResponsePayload
+ {
+ public string Name { get; set; } = string.Empty;
+ public string Description { get; set; } = string.Empty;
+ public DateTime ExpiryDate { get; set; } = DateTime.MaxValue;
+ public string EmulationType { get; set; } = string.Empty;
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/Repsonse.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/Repsonse.cs
new file mode 100644
index 0000000..c26bef4
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/Repsonse.cs
@@ -0,0 +1,12 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class Response
+ {
+ public bool Success { get; set; }
+ // public Type Type { get; set; } = typeof(T);
+ public T? Payload { get; set; }
+ public ErrorResponsePayload? Error { get; set; }
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/Request.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/Request.cs
new file mode 100644
index 0000000..468c26c
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/Request.cs
@@ -0,0 +1,10 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class Request
+ {
+ public long Id { get; set; }
+ public T? Payload { get; set; }
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageRequest.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageRequest.cs
new file mode 100644
index 0000000..1f3ce21
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageRequest.cs
@@ -0,0 +1,9 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class SaveImageRequest : Request
+ {
+
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageRequestPayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageRequestPayload.cs
new file mode 100644
index 0000000..a021d36
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageRequestPayload.cs
@@ -0,0 +1,10 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class SaveImageRequestPayload
+ {
+ public string UnitId { get; set; } = string.Empty;
+ public byte[]? Data { get; set; }
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageResponse.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageResponse.cs
new file mode 100644
index 0000000..90517b0
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageResponse.cs
@@ -0,0 +1,9 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class SaveImageResponse : Response
+ {
+
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageResponsePayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageResponsePayload.cs
new file mode 100644
index 0000000..e172967
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageResponsePayload.cs
@@ -0,0 +1,9 @@
+using System.Numerics;
+
+namespace AquaCubeIT.Service.NetFloppy.Models
+{
+ public class SaveImageResponsePayload
+ {
+ public int BytesSaved { get; set; }
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Program.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Program.cs
new file mode 100644
index 0000000..58f3ce8
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Program.cs
@@ -0,0 +1,56 @@
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.IdentityModel.Tokens;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+
+builder.Services.AddControllers();
+// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
+builder.Services.AddOpenApi();
+builder.Services.ConfigureHttpJsonOptions(options =>
+{
+ options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+ options.SerializerOptions.WriteIndented = true;
+ options.SerializerOptions.UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow;
+});
+builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
+{
+ options.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidateIssuer = true,
+ ValidateAudience = true,
+ ValidateLifetime = true,
+ ValidateIssuerSigningKey = true,
+ ValidIssuer = builder.Configuration["Jwt:Issuer"],
+ ValidAudience = builder.Configuration["Jwt:Audience"],
+ IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
+ };
+});
+builder.Services.AddAuthorization(options =>
+{
+ options.AddPolicy("MustBeAdmin", p => p.RequireRole("Admin"));
+ options.AddPolicy("MustBeUser", p => p.RequireRole("Admin", "User"));
+ // options.AddPolicy("CanReadOrders", p => p.RequireClaim("permission", "orders.read"));
+});
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.MapOpenApi();
+}
+
+
+//app.UseHttpsRedirection();
+
+app.UseAuthentication();
+app.UseAuthorization();
+
+app.MapControllers();
+
+app.Run();
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Properties/launchSettings.json b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Properties/launchSettings.json
new file mode 100644
index 0000000..9a441a6
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Properties/launchSettings.json
@@ -0,0 +1,31 @@
+{
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "dotnetRunMessages": true,
+ "applicationUrl": "http://localhost:5250"
+ },
+ "https": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "dotnetRunMessages": true,
+ "applicationUrl": "https://localhost:7289;http://localhost:5250"
+ },
+ "Container (Dockerfile)": {
+ "commandName": "Docker",
+ "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
+ "environmentVariables": {
+ "ASPNETCORE_HTTPS_PORTS": "8081",
+ "ASPNETCORE_HTTP_PORTS": "8080"
+ },
+ "publishAllPorts": true,
+ "useSSL": true
+ }
+ },
+ "$schema": "https://json.schemastore.org/launchsettings.json"
+}
\ No newline at end of file
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/TestDisk.adf b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/TestDisk.adf
new file mode 100644
index 0000000..344d7c1
Binary files /dev/null and b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/TestDisk.adf differ
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/appsettings.Development.json b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/appsettings.json b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/appsettings.json
new file mode 100644
index 0000000..a2ef3ed
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/appsettings.json
@@ -0,0 +1,20 @@
+{
+ "Kestrel": {
+ "Endpoints": {
+ "Http": { "Url": "http://0.0.0.0:5250" },
+ "Https": { "Url": "https://0.0.0.0:5251" }
+ }
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "Microsoft.AspNetCore": "Debug"
+ }
+ },
+ "AllowedHosts": "*",
+ "Jwt": {
+ "Key": "k&FS^docD5woeaDVczTZfmb44ijwm%W9AfE`Zf2h#%wjc",
+ "Issuer": "NetFloppy",
+ "Audience": "NetFloppy User"
+ }
+}
diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/libman.json b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/libman.json
new file mode 100644
index 0000000..8ed1410
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/libman.json
@@ -0,0 +1,5 @@
+{
+ "version": "3.0",
+ "defaultProvider": "cdnjs",
+ "libraries": []
+}
\ No newline at end of file
diff --git a/AquaCubeIT.Service.NetFloppy/TEst/TEst.dbmdl b/AquaCubeIT.Service.NetFloppy/TEst/TEst.dbmdl
new file mode 100644
index 0000000..aa415aa
Binary files /dev/null and b/AquaCubeIT.Service.NetFloppy/TEst/TEst.dbmdl differ
diff --git a/AquaCubeIT.Service.NetFloppy/TEst/TEst.sqlproj b/AquaCubeIT.Service.NetFloppy/TEst/TEst.sqlproj
new file mode 100644
index 0000000..b6dd4e1
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/TEst/TEst.sqlproj
@@ -0,0 +1,63 @@
+
+
+
+ Debug
+ AnyCPU
+ TEst
+ 2.0
+ 4.1
+ {89a44dbb-da85-4401-8b94-ed76e238e3bb}
+ Microsoft.Data.Tools.Schema.Sql.Sql160DatabaseSchemaProvider
+ Database
+
+
+ TEst
+ TEst
+ 1033, CI
+ BySchemaAndSchemaType
+ True
+ v4.7.2
+ CS
+ Properties
+ False
+ True
+ True
+
+
+ bin\Release\
+ $(MSBuildProjectName).sql
+ False
+ pdbonly
+ true
+ false
+ true
+ prompt
+ 4
+
+
+ bin\Debug\
+ $(MSBuildProjectName).sql
+ false
+ true
+ full
+ false
+ true
+ true
+ prompt
+ 4
+
+
+ 11.0
+
+ True
+ 11.0
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AquaCubeIT.Service.NetFloppy/TEst/TEst.sqlproj.user b/AquaCubeIT.Service.NetFloppy/TEst/TEst.sqlproj.user
new file mode 100644
index 0000000..0b07de1
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/TEst/TEst.sqlproj.user
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/AquaCubeIT.Service.NetFloppy/TEst/Table1.sql b/AquaCubeIT.Service.NetFloppy/TEst/Table1.sql
new file mode 100644
index 0000000..dde0eff
--- /dev/null
+++ b/AquaCubeIT.Service.NetFloppy/TEst/Table1.sql
@@ -0,0 +1,20 @@
+CREATE TABLE [dbo].[Table1]
+(
+ [Id] INT NOT NULL,
+ [ledger_start_transaction_id] bigint GENERATED ALWAYS AS TRANSACTION_ID START HIDDEN NOT NULL,
+ [ledger_start_sequence_number] bigint GENERATED ALWAYS AS SEQUENCE_NUMBER START
+)
+WITH
+(
+ LEDGER = ON
+ (
+ APPEND_ONLY = ON,
+ LEDGER_VIEW = [dbo].[Table1_Ledger]
+ (
+ transaction_id_column_name = [ledger_transaction_id],
+ sequence_number_column_name = [ledger_sequence_number],
+ operation_type_column_name = [ledger_operation_type],
+ operation_type_desc_column_name = [ledger_operation_type_desc]
+ )
+ )
+)
\ No newline at end of file
diff --git a/AquaCubeIT.Service.NetFloppy/Dockerfile b/Old/Dockerfile
similarity index 100%
rename from AquaCubeIT.Service.NetFloppy/Dockerfile
rename to Old/Dockerfile
diff --git a/Old/Fat16Disk.cs b/Old/Fat16Disk.cs
new file mode 100644
index 0000000..223a87b
--- /dev/null
+++ b/Old/Fat16Disk.cs
@@ -0,0 +1,335 @@
+// Fat16ImageLib.cs
+// Minimal C# library for creating a FAT16 disk image and adding files (root directory only)
+// Target: .NET 6+
+// Limitations: 8.3 filenames, root directory only (no subdirectories), no long file names, no deletions/compaction.
+
+using System;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace Fat16Lib
+{
+ public class Fat16ImageOptions
+ {
+ // Total size of the image in bytes. Common sizes: 16 MiB, 32 MiB, 64 MiB, 128 MiB, 256 MiB, etc.
+ public long TotalSizeBytes { get; set; } = 32L * 1024 * 1024; // 32 MiB default
+ public ushort BytesPerSector { get; set; } = 512;
+ public byte SectorsPerCluster { get; set; } = 0; // 0 = auto-select based on size
+ public byte NumFats { get; set; } = 2;
+ public ushort ReservedSectors { get; set; } = 1; // boot sector
+ public ushort MaxRootDirEntries { get; set; } = 512; // typical for FAT16
+ public byte MediaDescriptor { get; set; } = 0xF8; // fixed disk
+ public ushort SectorsPerTrack { get; set; } = 32; // geometry hints (not critical)
+ public ushort NumberOfHeads { get; set; } = 64; // geometry hints (not critical)
+ public uint HiddenSectors { get; set; } = 0;
+ public string OemId { get; set; } = "MSDOS5.0";
+ public string VolumeLabel { get; set; } = "NO NAME "; // exactly 11 chars; will be padded/trimmed
+ public string FsTypeLabel { get; set; } = "FAT16 "; // 8 chars in boot sector label
+ public ushort? VolumeId { get; set; } = null; // null => random
+ }
+
+ public class Fat16Image
+ {
+ private readonly Fat16ImageOptions _opt;
+
+ // Computed layout
+ private uint _totalSectors; // 16-bit or 32-bit depending on size
+ private ushort _rootDirSectors;
+ private ushort _fatSectors; // sectors per FAT
+ private uint _firstFatSectorLba;
+ private uint _firstRootDirSectorLba;
+ private uint _firstDataSectorLba;
+ private uint _clusterCount; // data clusters (not counting 0 and 1)
+ private byte _sectorsPerCluster;
+
+ // Runtime buffers
+ private byte[] _bootSector = Array.Empty();
+ private byte[] _fat; // a single FAT table; will be duplicated NumFats times
+ private byte[] _rootDir; // root directory area (fixed size)
+ private byte[] _data; // data region sized to contain all clusters (clusterCount * spc * bps)
+
+ // Allocation state
+ private uint _nextFreeCluster = 2; // FAT clusters start at 2
+ private int _nextFreeRootDirEntry = 0;
+
+ public Fat16Image(Fat16ImageOptions options)
+ {
+ _opt = options;
+ if (_opt.BytesPerSector != 512)
+ throw new NotSupportedException("Only 512-byte sectors supported in this minimal implementation.");
+ if (_opt.TotalSizeBytes % _opt.BytesPerSector != 0)
+ throw new ArgumentException("TotalSizeBytes must be a multiple of BytesPerSector.");
+
+ InitializeLayout();
+ BuildBootSector();
+ InitializeFatAndRoot();
+ }
+
+ // Adds a file into the root directory. Uses 8.3 name rules.
+ public void AddFile(string fileName, byte[] content, DateTime? timestamp = null)
+ {
+ if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentException("fileName");
+ if (content == null) throw new ArgumentNullException(nameof(content));
+
+ var (name83, ext83) = ToShortName(fileName);
+ if (_nextFreeRootDirEntry >= _opt.MaxRootDirEntries)
+ throw new IOException("Root directory is full.");
+
+ // Compute how many clusters needed
+ uint bytesPerCluster = (uint)(_opt.BytesPerSector * _sectorsPerCluster);
+ uint clustersNeeded = (uint)((content.Length + bytesPerCluster - 1) / bytesPerCluster);
+ if (clustersNeeded == 0) clustersNeeded = 1; // zero-length file occupies 0 clusters in FAT16 spec? Practically some tools allow 0. We'll allocate 1 for simplicity.
+
+ if ((_nextFreeCluster - 2) + clustersNeeded > _clusterCount)
+ throw new IOException("Not enough free space for file.");
+
+ // Allocate cluster chain
+ List chain = new();
+ for (int i = 0; i < clustersNeeded; i++)
+ {
+ uint c = _nextFreeCluster++;
+ chain.Add(c);
+ }
+
+ // Write data to clusters
+ int srcOffset = 0;
+ foreach (var c in chain)
+ {
+ uint lba = ClusterToLba(c);
+ uint offset = (lba - _firstDataSectorLba) * _opt.BytesPerSector;
+ int toCopy = (int)Math.Min(bytesPerCluster, (uint)(content.Length - srcOffset));
+ if (toCopy > 0)
+ Buffer.BlockCopy(content, srcOffset, _data, (int)offset, toCopy);
+ // Zero the remainder of the cluster (already zeroed by default construction)
+ srcOffset += toCopy;
+ }
+
+ // Link FAT entries
+ for (int i = 0; i < chain.Count - 1; i++)
+ SetFatEntry(chain[i], (ushort)chain[i + 1]);
+ SetFatEntry(chain[^1], 0xFFFF); // EOF
+
+ // Build directory entry
+ var dt = timestamp ?? DateTime.Now;
+ ushort time = ToFatTime(dt);
+ ushort date = ToFatDate(dt);
+
+ Span entry = stackalloc byte[32];
+ entry.Clear();
+ Encoding.ASCII.GetBytes(name83, entry);
+ Encoding.ASCII.GetBytes(ext83, entry.Slice(8));
+ entry[11] = 0x20; // Archive
+ BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(14), time); // Create time
+ BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(16), date); // Create date
+ BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(18), date); // Last access date (approx)
+ BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(22), time); // Write time
+ BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(24), date); // Write date
+ BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(26), (ushort)chain[0]); // First cluster
+ BinaryPrimitives.WriteUInt32LittleEndian(entry.Slice(28), (uint)content.Length); // File size
+
+ // Write entry into root dir
+ int rootOffset = _nextFreeRootDirEntry * 32;
+ _rootDir.AsSpan(rootOffset, 32).Clear();
+ entry.CopyTo(_rootDir.AsSpan(rootOffset));
+ _nextFreeRootDirEntry++;
+ }
+
+ public byte[] GetImage()
+ {
+ using var ms = new MemoryStream((int)(_opt.TotalSizeBytes));
+
+ // Boot sector
+ ms.Write(_bootSector, 0, _bootSector.Length);
+
+ // FATs
+ for (int f = 0; f < _opt.NumFats; f++)
+ ms.Write(_fat, 0, _fat.Length);
+
+ // Root directory
+ ms.Write(_rootDir, 0, _rootDir.Length);
+
+ // Data region
+ ms.Write(_data, 0, _data.Length);
+
+ // Pad to exact size if necessary (should be exact)
+ if (ms.Length < _opt.TotalSizeBytes)
+ ms.Write(new byte[_opt.TotalSizeBytes - ms.Length], 0, (int)(_opt.TotalSizeBytes - ms.Length));
+
+ return ms.ToArray();
+ }
+
+ public void SaveToFile(string path)
+ {
+ File.WriteAllBytes(path, GetImage());
+ }
+
+ private void InitializeLayout()
+ {
+ _totalSectors = (uint)(_opt.TotalSizeBytes / _opt.BytesPerSector);
+
+ // Decide sectors per cluster if not specified, per rough guidance for FAT16
+ _sectorsPerCluster = _opt.SectorsPerCluster != 0 ? _opt.SectorsPerCluster : SelectSectorsPerCluster(_totalSectors, _opt.BytesPerSector);
+
+ _rootDirSectors = (ushort)(((uint)_opt.MaxRootDirEntries * 32 + (_opt.BytesPerSector - 1)) / _opt.BytesPerSector);
+
+ // We need to iteratively solve for FAT size and cluster count
+ ushort fatSectors = 1;
+ while (true)
+ {
+ uint dataSectors = _totalSectors - _opt.ReservedSectors - (uint)(_opt.NumFats * fatSectors) - _rootDirSectors;
+ uint clusterCount = dataSectors / _sectorsPerCluster;
+ if (clusterCount < 4085 || clusterCount >= 65525)
+ throw new NotSupportedException($"Cluster count {clusterCount} out of FAT16 bounds (needs 4,085..65,524). Adjust size or sectors/cluster.");
+
+ // FAT16 entry size 2 bytes, need (clusterCount + 2) entries
+ ushort requiredFatSectors = (ushort)(((clusterCount + 2) * 2 + (_opt.BytesPerSector - 1)) / _opt.BytesPerSector);
+ if (requiredFatSectors == fatSectors)
+ {
+ _fatSectors = fatSectors;
+ _clusterCount = clusterCount;
+ break;
+ }
+ fatSectors = requiredFatSectors;
+ }
+
+ _firstFatSectorLba = _opt.ReservedSectors;
+ _firstRootDirSectorLba = _firstFatSectorLba + (uint)(_opt.NumFats * _fatSectors);
+ _firstDataSectorLba = _firstRootDirSectorLba + _rootDirSectors;
+
+ // Buffers
+ _bootSector = new byte[_opt.BytesPerSector];
+ _fat = new byte[_fatSectors * _opt.BytesPerSector];
+ _rootDir = new byte[_rootDirSectors * _opt.BytesPerSector];
+ uint dataSectorsFinal = _totalSectors - _opt.ReservedSectors - (uint)(_opt.NumFats * _fatSectors) - _rootDirSectors;
+ _data = new byte[dataSectorsFinal * _opt.BytesPerSector];
+ }
+
+ private void BuildBootSector()
+ {
+ Span bs = _bootSector;
+ bs.Clear();
+
+ // Jump + OEM
+ bs[0] = 0xEB; bs[1] = 0x3C; bs[2] = 0x90; // JMP short
+ Encoding.ASCII.GetBytes((_opt.OemId ?? "MSDOS5.0").PadRight(8).Substring(0,8), bs.Slice(3, 8));
+
+ // BIOS Parameter Block (BPB)
+ BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(11), _opt.BytesPerSector);
+ bs[13] = _sectorsPerCluster;
+ BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(14), _opt.ReservedSectors);
+ bs[16] = _opt.NumFats;
+ BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(17), _opt.MaxRootDirEntries);
+ ushort totalSectors16 = _totalSectors <= 0xFFFF ? (ushort)_totalSectors : (ushort)0;
+ BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(19), totalSectors16);
+ bs[21] = _opt.MediaDescriptor;
+ BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(22), _fatSectors);
+ BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(24), _opt.SectorsPerTrack);
+ BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(26), _opt.NumberOfHeads);
+ BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(28), _opt.HiddenSectors);
+ BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(32), _totalSectors);
+
+ // Extended BPB for FAT16
+ ushort volId = _opt.VolumeId ?? (ushort)Random.Shared.Next(1, 0xFFFF);
+ BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(39), volId);
+ string label = (_opt.VolumeLabel ?? "NO NAME").ToUpperInvariant();
+ label = new string(label.Where(ch => ch >= 0x20 && ch <= 0x7E).ToArray());
+ label = label.PadRight(11).Substring(0, 11);
+ Encoding.ASCII.GetBytes(label, bs.Slice(43, 11));
+ string fstype = (_opt.FsTypeLabel ?? "FAT16").PadRight(8).Substring(0, 8);
+ Encoding.ASCII.GetBytes(fstype, bs.Slice(54, 8));
+
+ // Minimal bootstrap code area (ignored for data disks)
+ bs[_opt.BytesPerSector - 2] = 0x55; // signature
+ bs[_opt.BytesPerSector - 1] = 0xAA;
+ }
+
+ private void InitializeFatAndRoot()
+ {
+ // FAT initial entries
+ // Entry 0: media descriptor in low byte, 0xFF in high byte
+ SetFatEntryRaw(0, (ushort)(_opt.MediaDescriptor | 0xFF00));
+ // Entry 1: EOF
+ SetFatEntryRaw(1, 0xFFFF);
+ // Zero rest (already zero)
+ }
+
+ private uint ClusterToLba(uint cluster)
+ {
+ if (cluster < 2) throw new ArgumentOutOfRangeException(nameof(cluster));
+ return _firstDataSectorLba + (cluster - 2) * _sectorsPerCluster;
+ }
+
+ private void SetFatEntry(uint cluster, ushort value)
+ {
+ if (cluster >= _clusterCount + 2) throw new ArgumentOutOfRangeException(nameof(cluster));
+ SetFatEntryRaw(cluster, value);
+ }
+
+ private void SetFatEntryRaw(uint cluster, ushort value)
+ {
+ int index = (int)(cluster * 2);
+ BinaryPrimitives.WriteUInt16LittleEndian(_fat.AsSpan(index, 2), value);
+ }
+
+ private static (string name, string ext) ToShortName(string fileName)
+ {
+ // Extract base and extension, sanitize to 8.3 uppercase ASCII
+ string name = Path.GetFileNameWithoutExtension(fileName) ?? "FILE";
+ string ext = Path.GetExtension(fileName);
+ if (ext.StartsWith('.')) ext = ext.Substring(1);
+
+ string Sanitize(string s) => new string(s.ToUpperInvariant()
+ .Select(c => (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || "!#$%&'()-@^_`{}~".Contains(c) ? c : '_')
+ .ToArray());
+
+ string n = Sanitize(name);
+ string e = Sanitize(ext);
+ if (n.Length > 8) n = n.Substring(0, 8);
+ if (e.Length > 3) e = e.Substring(0, 3);
+ n = n.PadRight(8);
+ e = e.PadRight(3);
+ return (n, e);
+ }
+
+ private static ushort ToFatDate(DateTime dt)
+ {
+ int year = Math.Clamp(dt.Year - 1980, 0, 127);
+ int month = Math.Clamp(dt.Month, 1, 12);
+ int day = Math.Clamp(dt.Day, 1, 31);
+ return (ushort)((year << 9) | (month << 5) | day);
+ }
+
+ private static ushort ToFatTime(DateTime dt)
+ {
+ int hour = Math.Clamp(dt.Hour, 0, 23);
+ int minute = Math.Clamp(dt.Minute, 0, 59);
+ int second = Math.Clamp(dt.Second / 2, 0, 29); // 2-second resolution
+ return (ushort)((hour << 11) | (minute << 5) | second);
+ }
+
+ private static byte SelectSectorsPerCluster(uint totalSectors, ushort bytesPerSector)
+ {
+ // Very rough mapping consistent with FAT16 recommendations for 512-byte sectors
+ // Values chosen to keep cluster count in FAT16 range and cluster size reasonable
+ long sizeMiB = (long)totalSectors * bytesPerSector / (1024 * 1024);
+ return sizeMiB switch
+ {
+ <= 16 => 2, // 1 KiB clusters
+ <= 32 => 4, // 2 KiB
+ <= 64 => 8, // 4 KiB
+ <= 128 => 16, // 8 KiB
+ <= 256 => 32, // 16 KiB
+ <= 512 => 64, // 32 KiB
+ _ => 64, // 32 KiB clusters for larger images in this minimal impl
+ };
+ }
+ }
+}
+
+// --------- Example usage (not part of the library) ---------
+// var img = new Fat16Lib.Fat16Image(new Fat16Lib.Fat16ImageOptions { TotalSizeBytes = 32L * 1024 * 1024, VolumeLabel = "MYDISK" });
+// img.AddFile("HELLO.TXT", Encoding.ASCII.GetBytes("Hello FAT16!\r\n"));
+// img.SaveToFile("fat16.img");
diff --git a/AquaCubeIT.Service.NetFloppy/Program.cs b/Old/Program.cs
similarity index 100%
rename from AquaCubeIT.Service.NetFloppy/Program.cs
rename to Old/Program.cs
diff --git a/AquaCubeIT.Service.NetFloppy/appsettings.json b/Old/appsettings.json
similarity index 100%
rename from AquaCubeIT.Service.NetFloppy/appsettings.json
rename to Old/appsettings.json
diff --git a/AquaCubeIT.Service.NetFloppy/docker-compose.yml b/Old/docker-compose.yml
similarity index 100%
rename from AquaCubeIT.Service.NetFloppy/docker-compose.yml
rename to Old/docker-compose.yml
diff --git a/AquaCubeIT.Service.NetFloppy/fat_16_image_lib.cs b/Old/fat_16_image_lib.cs
similarity index 100%
rename from AquaCubeIT.Service.NetFloppy/fat_16_image_lib.cs
rename to Old/fat_16_image_lib.cs