Skip to content

Directory Snapshots

This example serves the content of a directory as a read-only repository. Every time the content changes, a new commit is added on top of the previous one, so clients can simply git pull to receive updates.

using GenHTTP.Modules.Git;

/// <summary>
/// Serves snapshots of a directory, creating a new commit whenever
/// its content changed since the last snapshot.
/// </summary>
public sealed class DirectorySnapshots(DirectoryInfo directory) : IGitRepository
{
    private readonly InMemoryGitRepository _history = new();

    private GitObjectId? _lastTree;

    public async Task RefreshAsync()
    {
        var tree = GitTree.Create();

        foreach (var file in directory.EnumerateFiles("*", SearchOption.AllDirectories))
        {
            var path = Path.GetRelativePath(directory.FullName, file.FullName).Replace('\\', '/');

            // read lazily, the content is only needed to compute ids and to serve it
            tree.Add(path, async () => await File.ReadAllBytesAsync(file.FullName));
        }

        var snapshot = tree.Build();

        var id = await snapshot.ComputeIdAsync();

        if (id == _lastTree)
        {
            return;
        }

        // keep the content in memory, as the files on disk will change
        var copy = GitTree.Create();

        foreach (var file in snapshot)
        {
            copy.Add(file.Path, await file.ReadAsync(), file.Mode);
        }

        await _history.CommitAsync("main", copy.Build(), $"Snapshot of {DateTime.UtcNow:u}");

        _lastTree = id;
    }

    public ValueTask<GitReferences> GetReferencesAsync() => _history.GetReferencesAsync();

    public ValueTask<GitCommit?> GetCommitAsync(GitObjectId id) => _history.GetCommitAsync(id);

    public ValueTask<GitTree> GetTreeAsync(GitCommit commit) => _history.GetTreeAsync(commit);

}
var snapshots = new DirectorySnapshots(new DirectoryInfo("./content"));

await snapshots.RefreshAsync();

var git = GitServer.Create()
                   .Repository(snapshots);

await Host.Create()
          .Handler(git)
          .RunAsync();

Call RefreshAsync() whenever the content changes (e.g. from a FileSystemWatcher or a timer). As the repository only implements IGitRepository, clients cannot push to it.

Snapshots and history

The history only lives in memory, so it starts over when the application restarts. As the commits carry the time of the snapshot, the first commit after a restart has a different id than before, and clients receive a forced update with an unrelated history. To keep the history across restarts, persist GitCommit.Data and the files of every snapshot, as described in How It Works.