diff --git a/Api.SeaHavenIndustries/Infrastructure/PortAdapters.cs b/Api.SeaHavenIndustries/Infrastructure/PortAdapters.cs
index adcc357..a3ebbf3 100644
--- a/Api.SeaHavenIndustries/Infrastructure/PortAdapters.cs
+++ b/Api.SeaHavenIndustries/Infrastructure/PortAdapters.cs
@@ -70,6 +70,33 @@ namespace Api.SeaHavenIndustries.Infrastructure
}
}
+ public Stream? OpenRead(string fileUrl)
+ {
+ if (string.IsNullOrWhiteSpace(fileUrl))
+ return null;
+
+ try
+ {
+ if (!Uri.TryCreate(fileUrl, UriKind.Absolute, out var uri))
+ return null;
+
+ var relativePath = uri.AbsolutePath.TrimStart('/');
+ if (string.IsNullOrWhiteSpace(relativePath)
+ || relativePath.Contains("..", StringComparison.Ordinal)
+ || !relativePath.StartsWith("Assets/Documents/", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ var fullPath = Path.Combine(ResolveWebRoot(), relativePath.Replace('/', Path.DirectorySeparatorChar));
+ return System.IO.File.Exists(fullPath) ? System.IO.File.OpenRead(fullPath) : null;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
private string ResolveWebRoot()
=> _webHostEnvironment.WebRootPath
?? Path.Combine(_webHostEnvironment.ContentRootPath, "wwwroot");
diff --git a/SeaHaven.Services/Interfaces/IServicePorts.cs b/SeaHaven.Services/Interfaces/IServicePorts.cs
index 10c1718..c2d00a6 100644
--- a/SeaHaven.Services/Interfaces/IServicePorts.cs
+++ b/SeaHaven.Services/Interfaces/IServicePorts.cs
@@ -24,6 +24,12 @@ namespace SeaHaven.Services.Interfaces
/// cannot be resolved or removed; never throws for missing paths.
///
bool TryDelete(string fileUrl);
+
+ ///
+ /// Opens a previously saved file URL for reading. Returns null when the URL or file
+ /// cannot be safely resolved.
+ ///
+ Stream? OpenRead(string fileUrl);
}
///