< Summary

Information
Class: Api.Files.FileProviders.LocalFSFileProviderConfig
Assembly: Api
File(s): /home/runner/work/ProjectRead.ing/ProjectRead.ing/Api/Files/FileProviders/LocalFSFileProvider.cs
Line coverage
100%
Covered lines: 1
Uncovered lines: 0
Coverable lines: 1
Total lines: 111
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/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
 111public record LocalFSFileProviderConfig(string? BasePath);
 12
 13public class LocalFSFileProvider(LocalFSFileProviderConfig config) : IFileProvider
 14{
 15    public async Task<Stream?> GetFileAsync(Guid ownerId, Guid id, CancellationToken ct)
 16    {
 17        var path = System.IO.Path.Join(
 18            config.BasePath,
 19            ownerId.ToString(),
 20            id.ToString()
 21        );
 22
 23        // in fuse or network filesystems checking and opening file might be blocking non trivial amount of time
 24        return await Task.Run(
 25            () => System.IO.Path.Exists(path) ? File.OpenRead(path) : null,
 26            ct
 27        );
 28    }
 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    )
 38    {
 39        var tempDir = System.IO.Path.Join(config.BasePath, "uploading");
 40        var tmpPath = System.IO.Path.Join(
 41            tempDir,
 42            Random.Shared.GetHexString(128 / 4, true)
 43        );
 44        var storageDir = System.IO.Path.Join(config.BasePath, ownerId.ToString());
 45        var storagePath = System.IO.Path.Join(storageDir, id.ToString());
 46        Directory.CreateDirectory(tempDir);
 47        Directory.CreateDirectory(storageDir);
 48
 49        using var freeTempFile = Disposable.Create(() => File.Delete(tmpPath));
 50        using var handle = new FileStream(
 51            tmpPath,
 52            new FileStreamOptions
 53            {
 54                Mode = FileMode.Create,
 55                Access = FileAccess.Write,
 56                Options = FileOptions.Asynchronous,
 57                PreallocationSize = sizeBytes,
 58                UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite,
 59            }
 60        );
 61
 62        // see https://github.com/dotnet/dotnet/blob/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/
 63        const int defaultCopyBufferSize = 81920;
 64        var copyBuffer = ArrayPool<byte>.Shared.Rent(defaultCopyBufferSize);
 65        using var freeBuffer = Disposable.Create(() =>
 66            ArrayPool<byte>.Shared.Return(copyBuffer)
 67        );
 68        using var hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
 69        // Mostly Ai Generated - Start
 70        int bytesRead = 0;
 71        long totalBytesRead = 0;
 72        // Mostly Ai Generated - End
 73        while (
 74            (bytesRead = await stream.ReadAsync(new Memory<byte>(copyBuffer), ct)) != 0
 75        )
 76        {
 77            totalBytesRead += bytesRead;
 78            if (totalBytesRead > sizeBytes)
 79            {
 80                return false;
 81            }
 82            var data = new ReadOnlyMemory<byte>(copyBuffer, 0, bytesRead);
 83            hasher.AppendData(data.Span);
 84            await handle.WriteAsync(data, ct);
 85        }
 86
 87        // Mostly Ai Generated - Start
 88        if (totalBytesRead != sizeBytes)
 89        {
 90            return false;
 91        }
 92        // Mostly Ai Generated - End
 93
 94        if (!hasher.GetHashAndReset().SequenceEqual(sha256Hash))
 95        {
 96            return false;
 97        }
 98
 99        handle.Close();
 100        try
 101        {
 102            File.Move(tmpPath, storagePath, overwrite: true);
 103        }
 104        catch (IOException)
 105        {
 106            return false;
 107        }
 108
 109        return true;
 110    }
 111}

Methods/Properties

.ctor(string)