Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build/tasks.ps1
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@

$root = [System.IO.Path]::GetFullPath("$PSScriptRoot\..")

$sln_file = "$root\src\Sa.sln"
$sln_file = "$root\src\Sa.slnx"
$sln_platform = "Any CPU"
$config = "Release"
$dist_folder = "$root\dist"
$msbuild_verbosity = "n"

$projects = @(
$projects = @(
"Sa.Media",
"Sa.Media.FFmpeg",

Expand Down
13 changes: 7 additions & 6 deletions src/Sa.HybridFileStorage.Postgres/FileIdParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@
namespace Sa.HybridFileStorage.Postgres;

internal static class FileIdParser

{
private const string DateFormat = "yyyy/MM/dd/HH";

public static (int tenantId, long timestamp) ParseFromFileId(string fileId)
public static (int tenantId, long timestamp) ParseFromFileId(string fileId, string tableName)
{
if (string.IsNullOrWhiteSpace(fileId))
{
Expand All @@ -16,13 +15,15 @@ public static (int tenantId, long timestamp) ParseFromFileId(string fileId)

ReadOnlySpan<char> span = fileId.AsSpan();

int separatorIndex = span.IndexOf("://");
string seporator = $"://{tableName}/";

int separatorIndex = span.IndexOf(seporator);
if (separatorIndex == -1)
{
throw new FormatException("Invalid file ID format.");
}

ReadOnlySpan<char> subParts = span[(separatorIndex + 3)..]; // +3 for skip "://"
ReadOnlySpan<char> subParts = span[(separatorIndex + seporator.Length)..]; // +3 for skip "://files/"

int firstSlashIndex = subParts.IndexOf('/');
if (firstSlashIndex == -1)
Expand All @@ -48,8 +49,8 @@ public static (int tenantId, long timestamp) ParseFromFileId(string fileId)
}


public static string FormatToFileId(string storageType, int tenantId, DateTimeOffset date, string fileName)
=> $"{storageType}://{tenantId}/{date.ToString(DateFormat, CultureInfo.InvariantCulture)}/{NormalizeFileName(fileName)}";
public static string FormatToFileId(string storageType, string tableName, int tenantId, DateTimeOffset date, string fileName)
=> $"{storageType}://{tableName}/{tenantId}/{date.ToString(DateFormat, CultureInfo.InvariantCulture)}/{NormalizeFileName(fileName)}";

public static string NormalizeFileName(string fileName) => fileName.TrimStart('\\', '/').Replace('\\', '/');

Expand Down
11 changes: 5 additions & 6 deletions src/Sa.HybridFileStorage.Postgres/PostgresFileStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ internal sealed class PostgresFileStorage(
StorageOptions options,
TimeProvider? timeProvider = null) : IFileStorage
{

private readonly string _qualifiedTableName = $"{options.SchemaName}.\"{options.TableName.Trim('"')}\"";
private readonly string _qualifiedTableName = $"{options.SchemaName}.\"{options.TableName}\"";

public string StorageType { get; } = options.StorageType;

Expand All @@ -36,7 +35,7 @@ public async Task<StorageResult> UploadAsync(UploadFileInput metadata, Stream fi
await partManager.EnsureParts(_qualifiedTableName, now, [metadata.TenantId], cancellationToken);


string fileId = FileIdParser.FormatToFileId(StorageType, metadata.TenantId, now, metadata.FileName);
string fileId = FileIdParser.FormatToFileId(StorageType, options.TableName, metadata.TenantId, now, metadata.FileName);
string fileExtension = FileIdParser.GetFileExtension(metadata.FileName);

long createdAt = now.ToUnixTimeSeconds();
Expand Down Expand Up @@ -83,13 +82,13 @@ ON CONFLICT DO NOTHING
return new StorageResult(fileId, fileId, StorageType, now);
}

public bool CanProcess(string fileId) => fileId.StartsWith($"{StorageType}:://");
public bool CanProcess(string fileId) => fileId.StartsWith($"{StorageType}://{options.TableName}/");

public async Task<bool> DeleteAsync(string fileId, CancellationToken cancellationToken)
{
EnsureWritable();

(int tenantId, long timestamp) = FileIdParser.ParseFromFileId(fileId);
(int tenantId, long timestamp) = FileIdParser.ParseFromFileId(fileId, options.TableName);
int rowsAffected = await dataSource.ExecuteNonQuery(
$"""
DELETE FROM {_qualifiedTableName} WHERE tenant_id = @tenant_id AND created_at >= @timestamp AND id = @id
Expand All @@ -101,7 +100,7 @@ public async Task<bool> DeleteAsync(string fileId, CancellationToken cancellatio

public async Task<bool> DownloadAsync(string fileId, Func<Stream, CancellationToken, Task> loadStream, CancellationToken cancellationToken)
{
(int tenantId, long timestamp) = FileIdParser.ParseFromFileId(fileId);
(int tenantId, long timestamp) = FileIdParser.ParseFromFileId(fileId, options.TableName);

int rowsAffected = await dataSource.ExecuteReader(
$"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ public PostgresFileStorageConfiguration(IServiceCollection services)
var time = sp.GetService<TimeProvider>();
var sm = sp.GetRequiredService<RecyclableMemoryStreamManager>();

var storage = new PostgresFileStorage(dataSource, pm, sm, _options.StorageOptions, time);
StorageOptions options = _options.StorageOptions with
{
TableName = _options.StorageOptions.TableName.Trim('"')
};

var storage = new PostgresFileStorage(dataSource, pm, sm, options, time);
return storage;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Sa.HybridFileStorage.Postgres;

public class StorageOptions
public record StorageOptions
{
public string SchemaName { get; set; } = "public";
public string TableName { get; set; } = "files";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<Import Project="../Common.NuGet.Properties.xml" />

<PropertyGroup>
<Version>0.3.0</Version>
<Version>0.3.1</Version>
<Description>File storage management in Pg</Description>
</PropertyGroup>

Expand Down
19 changes: 19 additions & 0 deletions src/Sa.HybridFileStorage/Domain/StorageResult.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
namespace Sa.HybridFileStorage.Domain;

/// <summary>
/// Represents the result of a file upload operation.
/// FileId follows URI format: {storage_type}://{path_to_resource}[?parameters]
/// Examples:
/// <code>
/// // PostgreSQL storage
/// new StorageResult("pg://files/user_avatars/12345", "https://cdn.example.com/avatars/12345", "pg", DateTimeOffset.Now)
///
/// // Amazon S3 storage
/// new StorageResult("s3://my-bucket/documents/invoice.pdf", "https://my-bucket.s3.amazonaws.com/documents/invoice.pdf", "s3", DateTimeOffset.Now)
///
/// // Local file system storage
/// new StorageResult("file:///var/www/uploads/image.png", "/api/files/download/file/var/www/uploads/image.png", "file", DateTimeOffset.Now)
/// </code>
/// </summary>
/// <param name="FileId">Unique file identifier in URI format: {storage_type}://{path}[?params]</param>
/// <param name="AbsoluteUrl">Publicly accessible URL for downloading the file</param>
/// <param name="StorageType">Type of storage backend used ("pg", "s3", "file", "azure")</param>
/// <param name="UploadedAt">Timestamp when the file was uploaded</param>
public record StorageResult(string FileId, string AbsoluteUrl, string StorageType, DateTimeOffset UploadedAt);
Loading