@@ -0,0 +1,598 @@
+// Package file implements a backend that uses the filesystem to store non-Git related data
+//
+// The following files and directories are used:
+//
+//   - anon-access: contains the access level for anonymous users
+//   - allow-keyless: contains a boolean value indicating whether or not keyless access is allowed
+//   - admins: contains a list of authorized keys for admin users
+//   - host: contains the server's server hostname
+//   - name: contains the server's name
+//   - port: contains the server's port
+//   - repos: is a the directory containing all Git repositories
+//
+// Each repository has the following files and directories:
+//   - collaborators: contains a list of authorized keys for collaborators
+//   - description: contains the repository's description
+//   - private: when present, indicates that the repository is private
+//   - git-daemon-export-ok: when present, indicates that the repository is public
+//   - project-name: contains the repository's project name
+package file
+
+import (
+	"bufio"
+	"errors"
+	"fmt"
+	"io"
+	"io/fs"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	"github.com/charmbracelet/log"
+	"github.com/charmbracelet/soft-serve/git"
+	"github.com/charmbracelet/soft-serve/server/backend"
+	"github.com/gliderlabs/ssh"
+	gitm "github.com/gogs/git-module"
+	gossh "golang.org/x/crypto/ssh"
+)
+
+// sub file and directory names.
+const (
+	anonAccess   = "anon-access"
+	allowKeyless = "allow-keyless"
+	admins       = "admins"
+	serverHost   = "host"
+	serverName   = "name"
+	serverPort   = "port"
+	repos        = "repos"
+	collabs      = "collaborators"
+	description  = "description"
+	private      = "private"
+)
+
+var (
+	logger = log.WithPrefix("backend.file")
+)
+
+var _ backend.Backend = &FileBackend{}
+
+var _ backend.AccessMethod = &FileBackend{}
+
+// FileBackend is a backend that uses the filesystem.
+type FileBackend struct { // nolint:revive
+	// path is the path to the directory containing the repositories and config
+	// files.
+	path string
+	// AdditionalAdmins additional admins to the server.
+	AdditionalAdmins []string
+}
+
+func (fb *FileBackend) reposPath() string {
+	return filepath.Join(fb.path, repos)
+}
+
+func (fb *FileBackend) adminsPath() string {
+	return filepath.Join(fb.path, admins)
+}
+
+func (fb *FileBackend) collabsPath(repo string) string {
+	return filepath.Join(fb.reposPath(), repo, collabs)
+}
+
+func sanatizeRepo(repo string) string {
+	return strings.TrimSuffix(repo, ".git")
+}
+
+func readOneLine(path string) (string, error) {
+	f, err := os.Open(path)
+	if err != nil {
+		return "", err
+	}
+	defer f.Close() // nolint:errcheck
+	s := bufio.NewScanner(f)
+	s.Scan()
+	return s.Text(), s.Err()
+}
+
+func readAll(path string) (string, error) {
+	f, err := os.Open(path)
+	if err != nil {
+		return "", err
+	}
+
+	bts, err := io.ReadAll(f)
+	return string(bts), err
+}
+
+// NewFileBackend creates a new FileBackend.
+func NewFileBackend(path string) (*FileBackend, error) {
+	fb := &FileBackend{path: path}
+	for _, dir := range []string{repos} {
+		if err := os.MkdirAll(filepath.Join(path, dir), 0755); err != nil {
+			return nil, err
+		}
+	}
+	for _, file := range []string{admins, anonAccess, allowKeyless, serverHost, serverName, serverPort} {
+		if _, err := os.OpenFile(filepath.Join(path, file), os.O_RDONLY|os.O_CREATE, 0644); err != nil {
+			return nil, err
+		}
+	}
+	return fb, nil
+}
+
+// AccessLevel returns the access level for the given public key and repo.
+//
+// It implements backend.AccessMethod.
+func (fb *FileBackend) AccessLevel(repo string, pk gossh.PublicKey) backend.AccessLevel {
+	private := fb.IsPrivate(repo)
+	anon := fb.AnonAccess()
+	if pk != nil {
+		// Check if the key is an admin.
+		if fb.IsAdmin(pk) {
+			return backend.AdminAccess
+		}
+
+		// Check if the key is a collaborator.
+		if fb.IsCollaborator(pk, repo) {
+			if anon > backend.ReadWriteAccess {
+				return anon
+			}
+			return backend.ReadWriteAccess
+		}
+
+		// Check if repo is private.
+		if !private {
+			if anon > backend.ReadOnlyAccess {
+				return anon
+			}
+			return backend.ReadOnlyAccess
+		}
+	}
+
+	if private {
+		return backend.NoAccess
+	}
+
+	return anon
+}
+
+// AddAdmin adds a public key to the list of server admins.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) AddAdmin(pk gossh.PublicKey) error {
+	// Skip if the key already exists.
+	if fb.IsAdmin(pk) {
+		return nil
+	}
+
+	ak := backend.MarshalAuthorizedKey(pk)
+	f, err := os.OpenFile(fb.adminsPath(), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
+	if err != nil {
+		logger.Debug("failed to open admin keys file", "err", err, "path", fb.adminsPath())
+		return err
+	}
+
+	defer f.Close() //nolint:errcheck
+	_, err = fmt.Fprintln(f, ak)
+	return err
+}
+
+// AddCollaborator adds a public key to the list of collaborators for the given repo.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) AddCollaborator(pk gossh.PublicKey, repo string) error {
+	// Skip if the key already exists.
+	if fb.IsCollaborator(pk, repo) {
+		return nil
+	}
+
+	ak := backend.MarshalAuthorizedKey(pk)
+	repo = sanatizeRepo(repo) + ".git"
+	f, err := os.OpenFile(fb.collabsPath(repo), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
+	if err != nil {
+		logger.Debug("failed to open collaborators file", "err", err, "path", fb.collabsPath(repo))
+		return err
+	}
+
+	defer f.Close() //nolint:errcheck
+	_, err = fmt.Fprintln(f, ak)
+	return err
+}
+
+// AllowKeyless returns true if keyless access is allowed.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) AllowKeyless() bool {
+	line, err := readOneLine(filepath.Join(fb.path, allowKeyless))
+	if err != nil {
+		logger.Debug("failed to read allow-keyless file", "err", err)
+		return false
+	}
+
+	return line == "true"
+}
+
+// AnonAccess returns the level of anonymous access allowed.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) AnonAccess() backend.AccessLevel {
+	line, err := readOneLine(filepath.Join(fb.path, anonAccess))
+	if err != nil {
+		logger.Debug("failed to read anon-access file", "err", err)
+		return backend.NoAccess
+	}
+
+	switch line {
+	case backend.NoAccess.String():
+		return backend.NoAccess
+	case backend.ReadOnlyAccess.String():
+		return backend.ReadOnlyAccess
+	case backend.ReadWriteAccess.String():
+		return backend.ReadWriteAccess
+	case backend.AdminAccess.String():
+		return backend.AdminAccess
+	default:
+		return backend.NoAccess
+	}
+}
+
+// Description returns the description of the given repo.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) Description(repo string) string {
+	repo = sanatizeRepo(repo) + ".git"
+	r := &Repo{path: filepath.Join(fb.reposPath(), repo)}
+	return r.Description()
+}
+
+// IsAdmin checks if the given public key is a server admin.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) IsAdmin(pk gossh.PublicKey) bool {
+	// Check if the key is an additional admin.
+	ak := backend.MarshalAuthorizedKey(pk)
+	for _, admin := range fb.AdditionalAdmins {
+		if ak == admin {
+			return true
+		}
+	}
+
+	f, err := os.Open(fb.adminsPath())
+	if err != nil {
+		logger.Debug("failed to open admins file", "err", err, "path", fb.adminsPath())
+		return false
+	}
+
+	defer f.Close() //nolint:errcheck
+	s := bufio.NewScanner(f)
+	for s.Scan() {
+		apk, _, err := backend.ParseAuthorizedKey(s.Text())
+		if err != nil {
+			continue
+		}
+		if ssh.KeysEqual(apk, pk) {
+			return true
+		}
+	}
+
+	return false
+}
+
+// IsCollaborator returns true if the given public key is a collaborator on the
+// given repo.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) IsCollaborator(pk gossh.PublicKey, repo string) bool {
+	repo = sanatizeRepo(repo) + ".git"
+	f, err := os.Open(fb.collabsPath(repo))
+	if err != nil {
+		logger.Debug("failed to open collaborators file", "err", err, "path", fb.collabsPath(repo))
+		return false
+	}
+
+	defer f.Close() //nolint:errcheck
+	s := bufio.NewScanner(f)
+	for s.Scan() {
+		apk, _, err := backend.ParseAuthorizedKey(s.Text())
+		if err != nil {
+			continue
+		}
+		if ssh.KeysEqual(apk, pk) {
+			return true
+		}
+	}
+
+	return false
+}
+
+// IsPrivate returns true if the given repo is private.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) IsPrivate(repo string) bool {
+	repo = sanatizeRepo(repo) + ".git"
+	r := &Repo{path: filepath.Join(fb.reposPath(), repo)}
+	return r.IsPrivate()
+}
+
+// ServerHost returns the server host.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) ServerHost() string {
+	line, err := readOneLine(filepath.Join(fb.path, serverHost))
+	if err != nil {
+		logger.Debug("failed to read server-host file", "err", err)
+		return ""
+	}
+
+	return line
+}
+
+// ServerName returns the server name.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) ServerName() string {
+	line, err := readOneLine(filepath.Join(fb.path, serverName))
+	if err != nil {
+		logger.Debug("failed to read server-name file", "err", err)
+		return ""
+	}
+
+	return line
+}
+
+// ServerPort returns the server port.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) ServerPort() string {
+	line, err := readOneLine(filepath.Join(fb.path, serverPort))
+	if err != nil {
+		logger.Debug("failed to read server-port file", "err", err)
+		return ""
+	}
+
+	if _, err := strconv.Atoi(line); err != nil {
+		logger.Debug("failed to parse server-port file", "err", err)
+		return ""
+	}
+
+	return line
+}
+
+// SetAllowKeyless sets whether or not to allow keyless access.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) SetAllowKeyless(allow bool) error {
+	f, err := os.OpenFile(filepath.Join(fb.path, allowKeyless), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+	if err != nil {
+		return fmt.Errorf("failed to open allow-keyless file: %w", err)
+	}
+
+	defer f.Close() //nolint:errcheck
+	_, err = fmt.Fprintln(f, allow)
+	return err
+}
+
+// SetAnonAccess sets the anonymous access level.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) SetAnonAccess(level backend.AccessLevel) error {
+	f, err := os.OpenFile(filepath.Join(fb.path, anonAccess), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+	if err != nil {
+		return fmt.Errorf("failed to open anon-access file: %w", err)
+	}
+
+	defer f.Close() //nolint:errcheck
+	_, err = fmt.Fprintln(f, level.String())
+	return err
+}
+
+// SetDescription sets the description of the given repo.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) SetDescription(repo string, desc string) error {
+	f, err := os.OpenFile(filepath.Join(fb.reposPath(), repo, description), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+	if err != nil {
+		return fmt.Errorf("failed to open description file: %w", err)
+	}
+
+	defer f.Close() //nolint:errcheck
+	_, err = fmt.Fprintln(f, desc)
+	return err
+}
+
+// SetPrivate sets the private status of the given repo.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) SetPrivate(repo string, priv bool) error {
+	repo = sanatizeRepo(repo) + ".git"
+	daemonExport := filepath.Join(fb.reposPath(), repo, "git-daemon-export-ok")
+	if priv {
+		_ = os.Remove(daemonExport)
+		f, err := os.Create(filepath.Join(fb.reposPath(), repo, private))
+		if err != nil {
+			return fmt.Errorf("failed to create private file: %w", err)
+		}
+
+		_ = f.Close() //nolint:errcheck
+	} else {
+		// Create git-daemon-export-ok file if repo is public.
+		f, err := os.Create(daemonExport)
+		if err != nil {
+			logger.Warn("failed to create git-daemon-export-ok file", "err", err)
+		} else {
+			_ = f.Close() //nolint:errcheck
+		}
+	}
+	return nil
+}
+
+// SetServerHost sets the server host.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) SetServerHost(host string) error {
+	f, err := os.Create(filepath.Join(fb.path, serverHost))
+	if err != nil {
+		return fmt.Errorf("failed to create server-host file: %w", err)
+	}
+
+	defer f.Close() //nolint:errcheck
+	_, err = fmt.Fprintln(f, host)
+	return err
+}
+
+// SetServerName sets the server name.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) SetServerName(name string) error {
+	f, err := os.Create(filepath.Join(fb.path, serverName))
+	if err != nil {
+		return fmt.Errorf("failed to create server-name file: %w", err)
+	}
+
+	defer f.Close() //nolint:errcheck
+	_, err = fmt.Fprintln(f, name)
+	return err
+}
+
+// SetServerPort sets the server port.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) SetServerPort(port string) error {
+	f, err := os.Create(filepath.Join(fb.path, serverPort))
+	if err != nil {
+		return fmt.Errorf("failed to create server-port file: %w", err)
+	}
+
+	defer f.Close() //nolint:errcheck
+	_, err = fmt.Fprintln(f, port)
+	return err
+}
+
+// CreateRepository creates a new repository.
+//
+// Created repositories are always bare.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) CreateRepository(name string, private bool) (backend.Repository, error) {
+	name = sanatizeRepo(name) + ".git"
+	rp := filepath.Join(fb.reposPath(), name)
+	if _, err := git.Init(rp, true); err != nil {
+		logger.Debug("failed to create repository", "err", err)
+		return nil, err
+	}
+
+	fb.SetPrivate(name, private)
+
+	return &Repo{path: rp}, nil
+}
+
+// DeleteRepository deletes the given repository.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) DeleteRepository(name string) error {
+	name = sanatizeRepo(name) + ".git"
+	return os.RemoveAll(filepath.Join(fb.reposPath(), name))
+}
+
+// RenameRepository renames the given repository.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) RenameRepository(oldName string, newName string) error {
+	oldName = sanatizeRepo(oldName) + ".git"
+	newName = sanatizeRepo(newName) + ".git"
+	return os.Rename(filepath.Join(fb.reposPath(), oldName), filepath.Join(fb.reposPath(), newName))
+}
+
+// Repository finds the given repository.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) Repository(repo string) (backend.Repository, error) {
+	repo = sanatizeRepo(repo) + ".git"
+	rp := filepath.Join(fb.reposPath(), repo)
+	_, err := os.Stat(rp)
+	if !errors.Is(err, os.ErrExist) {
+		return nil, err
+	}
+
+	return &Repo{path: rp}, nil
+}
+
+// Repositories returns a list of all repositories.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) Repositories() ([]backend.Repository, error) {
+	repos := make([]backend.Repository, 0)
+	err := filepath.WalkDir(fb.reposPath(), func(path string, d fs.DirEntry, _ error) error {
+		// Skip non-directories.
+		if !d.IsDir() {
+			return nil
+		}
+
+		// Skip non-repositories.
+		if !strings.HasSuffix(path, ".git") {
+			return nil
+		}
+
+		repos = append(repos, &Repo{path: path})
+
+		return nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return repos, nil
+}
+
+// DefaultBranch returns the default branch of the given repository.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) DefaultBranch(repo string) (string, error) {
+	rr, err := fb.Repository(repo)
+	if err != nil {
+		logger.Debug("failed to get default branch", "err", err)
+		return "", err
+	}
+
+	r, err := rr.Repository()
+	if err != nil {
+		logger.Debug("failed to open repository for default branch", "err", err)
+		return "", err
+	}
+
+	head, err := r.HEAD()
+	if err != nil {
+		logger.Debug("failed to get HEAD for default branch", "err", err)
+		return "", err
+	}
+
+	return head.Name().Short(), nil
+}
+
+// SetDefaultBranch sets the default branch for the given repository.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) SetDefaultBranch(repo string, branch string) error {
+	rr, err := fb.Repository(repo)
+	if err != nil {
+		logger.Debug("failed to get repository for default branch", "err", err)
+		return err
+	}
+
+	r, err := rr.Repository()
+	if err != nil {
+		logger.Debug("failed to open repository for default branch", "err", err)
+		return err
+	}
+
+	if _, err := r.SymbolicRef(gitm.SymbolicRefOptions{
+		Name: "HEAD",
+		Ref:  gitm.RefsHeads + branch,
+	}); err != nil {
+		logger.Debug("failed to set default branch", "err", err)
+		return err
+	}
+
+	return nil
+}