< Summary

Information
Class: Api.Files.FileProviders.LocalFSFileProvider
Assembly: Api
File(s): /home/runner/work/ProjectRead.ing/ProjectRead.ing/Api/Files/FileProviders/LocalFSFileProvider.cs
Line coverage
89%
Covered lines: 61
Uncovered lines: 7
Coverable lines: 68
Total lines: 111
Line coverage: 89.7%
Branch coverage
62%
Covered branches: 5
Total branches: 8
Branch coverage: 62.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

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

File(s)

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

#LineLine coverage
 1// SPDX-FileCopyrightText: 2026 Alper Çelik <[email protected]>
 2//
 3// SPDX-License-Identifier: AGPL-3.0-or-later
 4
 5using System.Buffers;
 6using System.Reactive.Disposables;
 7using System.Security.Cryptography;
 8
 9namespace Api.Files.FileProviders;
 10
 11public record LocalFSFileProviderConfig(string? BasePath);
 12
 113public class LocalFSFileProvider(LocalFSFileProviderConfig config) : IFileProvider
 14{
 15    public async Task<Stream?> GetFileAsync(Guid ownerId, Guid id, CancellationToken ct)
 116    {
 117        var path = System.IO.Path.Join(
 118            config.BasePath,
 119            ownerId.ToString(),
 120            id.ToString()
 121        );
 22
 23        // in fuse or network filesystems checking and opening file might be blocking non trivial amount of time
 124        return await Task.Run(
 125            () => System.IO.Path.Exists(path) ? File.OpenRead(path) : null,
 126            ct
 127        );
 128    }
 29
 30    public async Task<bool> SetFileAsync(
 31        Guid ownerId,
 32        Guid id,
 33        Stream stream,
 34        byte[] sha256Hash,
 35        long sizeBytes,
 36        CancellationToken ct
 37    )
 138    {
 139        var tempDir = System.IO.Path.Join(config.BasePath, "uploading");
 140        var tmpPath = System.IO.Path.Join(
 141            tempDir,
 142            Random.Shared.GetHexString(128 / 4, true)
 143        );
 144        var storageDir = System.IO.Path.Join(config.BasePath, ownerId.ToString());
 145        var storagePath = System.IO.Path.Join(storageDir, id.ToString());
 146        Directory.CreateDirectory(tempDir);
 147        Directory.CreateDirectory(storageDir);
 48
 149        using var freeTempFile = Disposable.Create(() => File.Delete(tmpPath));
 150        using var handle = new FileStream(
 151            tmpPath,
 152            new FileStreamOptions
 153            {
 154                Mode = FileMode.Create,
 155                Access = FileAccess.Write,
 156                Options = FileOptions.Asynchronous,
 157                PreallocationSize = sizeBytes,
 158                UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite,
 159            }
 160        );
 61
 62        // see https://github.com/dotnet/dotnet/blob/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/
 63        const int defaultCopyBufferSize = 81920;
 164        var copyBuffer = ArrayPool<byte>.Shared.Rent(defaultCopyBufferSize);
 165        using var freeBuffer = Disposable.Create(() =>
 166            ArrayPool<byte>.Shared.Return(copyBuffer)
 167        );
 168        using var hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
 69        // Mostly Ai Generated - Start
 170        int bytesRead = 0;
 171        long totalBytesRead = 0;
 72        // Mostly Ai Generated - End
 173        while (
 174            (bytesRead = await stream.ReadAsync(new Memory<byte>(copyBuffer), ct)) != 0
 175        )
 176        {
 177            totalBytesRead += bytesRead;
 178            if (totalBytesRead > sizeBytes)
 079            {
 080                return false;
 81            }
 182            var data = new ReadOnlyMemory<byte>(copyBuffer, 0, bytesRead);
 183            hasher.AppendData(data.Span);
 184            await handle.WriteAsync(data, ct);
 185        }
 86
 87        // Mostly Ai Generated - Start
 188        if (totalBytesRead != sizeBytes)
 089        {
 090            return false;
 91        }
 92        // Mostly Ai Generated - End
 93
 194        if (!hasher.GetHashAndReset().SequenceEqual(sha256Hash))
 195        {
 196            return false;
 97        }
 98
 199        handle.Close();
 100        try
 1101        {
 1102            File.Move(tmpPath, storagePath, overwrite: true);
 1103        }
 0104        catch (IOException)
 0105        {
 0106            return false;
 107        }
 108
 1109        return true;
 1110    }
 111}