< Summary

Information
Class: Api.Files.FileProviders.FileContent
Assembly: Api
File(s): /home/runner/work/ProjectRead.ing/ProjectRead.ing/Api/Files/FileProviders/UserFileRouter.cs
Line coverage
100%
Covered lines: 7
Uncovered lines: 0
Coverable lines: 7
Total lines: 197
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%

File(s)

/home/runner/work/ProjectRead.ing/ProjectRead.ing/Api/Files/FileProviders/UserFileRouter.cs

#LineLine coverage
 1// SPDX-FileCopyrightText: 2026 Alper Çelik <[email protected]>
 2//
 3// SPDX-License-Identifier: AGPL-3.0-or-later
 4
 5using Api.Database;
 6using Api.Files.Models;
 7using Microsoft.EntityFrameworkCore;
 8using NodaTime;
 9
 10namespace Api.Files.FileProviders;
 11
 12public class UserFileRouter(
 13    IServiceProvider services,
 14    PGContext db,
 15    FileProviderFactory fileProviderFactory
 16)
 17{
 18    public async Task<Guid> GetUsersPrefferedStorage(Guid userId, FileKind fileKind)
 19    {
 20        return await db
 21            .FileProviderBackendConfigs.AsNoTracking()
 22            .Where(cfg => cfg.InstanceWide == true || cfg.OwnerId == userId)
 23            .Where(cfg =>
 24                cfg.DefaultFor.Contains(fileKind) || cfg.FallbackDefault == true
 25            )
 26            .OrderBy(cfg => cfg.InstanceWide)
 27            .ThenBy(cfg => cfg.FallbackDefault)
 28            .ThenBy(cfg => cfg.DefaultFor.Length)
 29            .ThenByDescending(cfg => cfg.Id)
 30            .Select(cfg => cfg.Id)
 31            .FirstAsync();
 32    }
 33
 34    public async Task<FileRecord> CreateFileAsync(
 35        Guid userId,
 36        FileKind fileKind,
 37        string contentType,
 38        // Mostly Ai Generated - Start
 39        string? originalFileName,
 40        long sizeBytes,
 41        // Mostly Ai Generated - End
 42        byte[] sha256Hash,
 43        // Mostly Ai Generated - Start
 44        CancellationToken ct
 45    // Mostly Ai Generated - End
 46    )
 47    {
 48        Guid providerBackendConfigId = await GetUsersPrefferedStorage(userId, fileKind);
 49        var now = Now();
 50        var fileRecord = new FileRecord
 51        {
 52            Id = Guid.CreateVersion7().WithPostfix(FileRecord.IdPostfix),
 53            MetadataAddedAt = now,
 54            MetadataUpdatedAt = now,
 55
 56            OwnerId = userId,
 57            FileKind = fileKind,
 58            ContentType = contentType,
 59            OriginalFileName = originalFileName,
 60            // Mostly Ai Generated - Start
 61            SizeBytes = sizeBytes,
 62            // Mostly Ai Generated - End
 63            SHA256 = sha256Hash,
 64            FileProviderBackendConfigId = providerBackendConfigId,
 65        };
 66        // Mostly Ai Generated - Start
 67        await db.FileRecords.AddAsync(fileRecord, ct);
 68        await db.SaveChangesAsync(ct);
 69        return fileRecord;
 70        // Mostly Ai Generated - End
 71    }
 72
 73    public async Task<FileContent?> GetFileAsync(
 74        Guid ownerId,
 75        Guid id,
 76        CancellationToken ct
 77    )
 78    {
 79        var fr = await db
 80            .FileRecords.Where(fr => fr.OwnerId == ownerId && fr.Id == id && fr.Uploaded)
 81            .Where(fr =>
 82                fr.FileProviderBackendConfig.OwnerId == ownerId
 83                || fr.FileProviderBackendConfig.InstanceWide
 84            )
 85            .Select(fr => new
 86            {
 87                fr.FileProviderBackendConfig.ProviderId,
 88                fr.FileProviderBackendConfig.ProviderConfig,
 89                fr.FileProviderBackendConfig.EncryptedSecrets,
 90
 91                fr.ContentType,
 92                fr.SHA256,
 93                fr.OriginalFileName,
 94                fr.MetadataAddedAt,
 95            })
 96            .FirstOrDefaultAsync(ct);
 97
 98        if (fr is null)
 99        {
 100            return null;
 101        }
 102
 103        var fileProvider = await fileProviderFactory.New(
 104            services,
 105            fr.ProviderId,
 106            fr.ProviderConfig,
 107            fr.EncryptedSecrets
 108        );
 109
 110        if (fileProvider is null)
 111        {
 112            return null;
 113        }
 114
 115        var stream = await fileProvider.GetFileAsync(ownerId, id, ct);
 116        if (stream is null)
 117        {
 118            return null;
 119        }
 120        return new FileContent(
 121            stream,
 122            fr.ContentType,
 123            fr.OriginalFileName,
 124            fr.SHA256,
 125            fr.MetadataAddedAt
 126        );
 127    }
 128
 129    // Mostly Ai Generated - Start
 130    /// <summary>
 131    /// Stores the content for an existing record of <paramref name="ownerId"/>.
 132    /// Returns the updated record, or <see langword="null"/> when the provider rejected the
 133    /// content because it did not match the declared size or SHA256. Throws when the record
 134    /// does not exist for the owner or the provider cannot be constructed.
 135    /// </summary>
 136    public async Task<FileRecord?> SetFileAsync(
 137        Guid ownerId,
 138        Guid id,
 139        Stream stream,
 140        CancellationToken ct
 141    )
 142    {
 143        var fr =
 144            await db
 145                .FileRecords.Include(fr => fr.FileProviderBackendConfig)
 146                .FirstOrDefaultAsync(
 147                    fr =>
 148                        fr.OwnerId == ownerId
 149                        && fr.Id == id
 150                        && (
 151                            fr.FileProviderBackendConfig.OwnerId == ownerId
 152                            || fr.FileProviderBackendConfig.InstanceWide
 153                        ),
 154                    ct
 155                )
 156            ?? throw new ArgumentException("file doesn't exist");
 157
 158        var fileProvider =
 159            await fileProviderFactory.New(
 160                services,
 161                fr.FileProviderBackendConfig.ProviderId,
 162                fr.FileProviderBackendConfig.ProviderConfig,
 163                fr.FileProviderBackendConfig.EncryptedSecrets
 164            ) ?? throw new NullReferenceException();
 165
 166        if (
 167            !await fileProvider.SetFileAsync(
 168                ownerId,
 169                id,
 170                stream,
 171                fr.SHA256,
 172                fr.SizeBytes,
 173                ct
 174            )
 175        )
 176        {
 177            return null;
 178        }
 179
 180        fr.Uploaded = true;
 181        fr.RowVersion += 1;
 182        fr.MetadataUpdatedAt = Now();
 183        await db.SaveChangesAsync(ct);
 184        return fr;
 185    }
 186    // Mostly Ai Generated - End
 187}
 188
 189// Mostly Ai Generated - Start
 1190public record FileContent(
 1191    Stream Stream,
 1192    string ContentType,
 1193    string? OriginalFileName,
 1194    byte[] SHA256,
 1195    Instant MetadataAddedAt
 1196);
 197// Mostly Ai Generated - End