< Summary

Information
Class: Api.Files.FileProviders.UserFileRouter
Assembly: Api
File(s): /home/runner/work/ProjectRead.ing/ProjectRead.ing/Api/Files/FileProviders/UserFileRouter.cs
Line coverage
96%
Covered lines: 121
Uncovered lines: 4
Coverable lines: 125
Total lines: 197
Line coverage: 96.8%
Branch coverage
66%
Covered branches: 4
Total branches: 6
Branch coverage: 66.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetUsersPrefferedStorage()100%11100%
CreateFileAsync()100%11100%
GetFileAsync()66.67%6690.7%
SetFileAsync()100%11100%

File(s)

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

#LineLine coverage
 1// SPDX-FileCopyrightText: 2026 Alper Çelik <alper@alper-celik.dev>
 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
 112public class UserFileRouter(
 113    IServiceProvider services,
 114    PGContext db,
 115    FileProviderFactory fileProviderFactory
 116)
 17{
 18    public async Task<Guid> GetUsersPrefferedStorage(Guid userId, FileKind fileKind)
 119    {
 120        return await db
 121            .FileProviderBackendConfigs.AsNoTracking()
 122            .Where(cfg => cfg.InstanceWide == true || cfg.OwnerId == userId)
 123            .Where(cfg =>
 124                cfg.DefaultFor.Contains(fileKind) || cfg.FallbackDefault == true
 125            )
 126            .OrderBy(cfg => cfg.InstanceWide)
 127            .ThenBy(cfg => cfg.FallbackDefault)
 128            .ThenBy(cfg => cfg.DefaultFor.Length)
 129            .ThenByDescending(cfg => cfg.Id)
 130            .Select(cfg => cfg.Id)
 131            .FirstAsync();
 132    }
 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    )
 147    {
 148        Guid providerBackendConfigId = await GetUsersPrefferedStorage(userId, fileKind);
 149        var now = Now();
 150        var fileRecord = new FileRecord
 151        {
 152            Id = Guid.CreateVersion7().WithPostfix(FileRecord.IdPostfix),
 153            MetadataAddedAt = now,
 154            MetadataUpdatedAt = now,
 155
 156            OwnerId = userId,
 157            FileKind = fileKind,
 158            ContentType = contentType,
 159            OriginalFileName = originalFileName,
 160            // Mostly Ai Generated - Start
 161            SizeBytes = sizeBytes,
 162            // Mostly Ai Generated - End
 163            SHA256 = sha256Hash,
 164            FileProviderBackendConfigId = providerBackendConfigId,
 165        };
 66        // Mostly Ai Generated - Start
 167        await db.FileRecords.AddAsync(fileRecord, ct);
 168        await db.SaveChangesAsync(ct);
 169        return fileRecord;
 70        // Mostly Ai Generated - End
 171    }
 72
 73    public async Task<FileContent?> GetFileAsync(
 74        Guid ownerId,
 75        Guid id,
 76        CancellationToken ct
 77    )
 178    {
 179        var fr = await db
 180            .FileRecords.Where(fr => fr.OwnerId == ownerId && fr.Id == id && fr.Uploaded)
 181            .Where(fr =>
 182                fr.FileProviderBackendConfig.OwnerId == ownerId
 183                || fr.FileProviderBackendConfig.InstanceWide
 184            )
 185            .Select(fr => new
 186            {
 187                fr.FileProviderBackendConfig.ProviderId,
 188                fr.FileProviderBackendConfig.ProviderConfig,
 189                fr.FileProviderBackendConfig.EncryptedSecrets,
 190
 191                fr.ContentType,
 192                fr.SHA256,
 193                fr.OriginalFileName,
 194                fr.MetadataAddedAt,
 195            })
 196            .FirstOrDefaultAsync(ct);
 97
 198        if (fr is null)
 199        {
 1100            return null;
 101        }
 102
 1103        var fileProvider = await fileProviderFactory.New(
 1104            services,
 1105            fr.ProviderId,
 1106            fr.ProviderConfig,
 1107            fr.EncryptedSecrets
 1108        );
 109
 1110        if (fileProvider is null)
 0111        {
 0112            return null;
 113        }
 114
 1115        var stream = await fileProvider.GetFileAsync(ownerId, id, ct);
 1116        if (stream is null)
 0117        {
 0118            return null;
 119        }
 1120        return new FileContent(
 1121            stream,
 1122            fr.ContentType,
 1123            fr.OriginalFileName,
 1124            fr.SHA256,
 1125            fr.MetadataAddedAt
 1126        );
 1127    }
 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    )
 1142    {
 1143        var fr =
 1144            await db
 1145                .FileRecords.Include(fr => fr.FileProviderBackendConfig)
 1146                .FirstOrDefaultAsync(
 1147                    fr =>
 1148                        fr.OwnerId == ownerId
 1149                        && fr.Id == id
 1150                        && (
 1151                            fr.FileProviderBackendConfig.OwnerId == ownerId
 1152                            || fr.FileProviderBackendConfig.InstanceWide
 1153                        ),
 1154                    ct
 1155                )
 1156            ?? throw new ArgumentException("file doesn't exist");
 157
 1158        var fileProvider =
 1159            await fileProviderFactory.New(
 1160                services,
 1161                fr.FileProviderBackendConfig.ProviderId,
 1162                fr.FileProviderBackendConfig.ProviderConfig,
 1163                fr.FileProviderBackendConfig.EncryptedSecrets
 1164            ) ?? throw new NullReferenceException();
 165
 1166        if (
 1167            !await fileProvider.SetFileAsync(
 1168                ownerId,
 1169                id,
 1170                stream,
 1171                fr.SHA256,
 1172                fr.SizeBytes,
 1173                ct
 1174            )
 1175        )
 1176        {
 1177            return null;
 178        }
 179
 1180        fr.Uploaded = true;
 1181        fr.RowVersion += 1;
 1182        fr.MetadataUpdatedAt = Now();
 1183        await db.SaveChangesAsync(ct);
 1184        return fr;
 1185    }
 186    // Mostly Ai Generated - End
 187}
 188
 189// Mostly Ai Generated - Start
 190public record FileContent(
 191    Stream Stream,
 192    string ContentType,
 193    string? OriginalFileName,
 194    byte[] SHA256,
 195    Instant MetadataAddedAt
 196);
 197// Mostly Ai Generated - End