Skip to content

Commit

Permalink
fix: add repo owner and separate lfs data for each repository
Browse files Browse the repository at this point in the history
Add repository owner
  • Loading branch information
aymanbagabas committed Aug 4, 2023
1 parent 3a61783 commit ea6b9a4
Show file tree
Hide file tree
Showing 25 changed files with 307 additions and 57 deletions.
4 changes: 2 additions & 2 deletions cmd/soft/migrate_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ var migrateConfig = &cobra.Command{
return fmt.Errorf("failed to copy repo: %w", err)
}

if _, err := sb.CreateRepository(ctx, dir.Name(), proto.RepositoryOptions{}); err != nil {
if _, err := sb.CreateRepository(ctx, dir.Name(), nil, proto.RepositoryOptions{}); err != nil {
fmt.Fprintf(os.Stderr, "failed to create repository: %s\n", err)
}
}
Expand Down Expand Up @@ -239,7 +239,7 @@ var migrateConfig = &cobra.Command{
}

// Create `.soft-serve` repository and add readme
if _, err := sb.CreateRepository(ctx, ".soft-serve", proto.RepositoryOptions{
if _, err := sb.CreateRepository(ctx, ".soft-serve", nil, proto.RepositoryOptions{
ProjectName: "Home",
Description: "Soft Serve home repository",
Hidden: true,
Expand Down
4 changes: 3 additions & 1 deletion server/backend/lfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"path"
"path/filepath"
"strconv"

"github.com/charmbracelet/soft-serve/server/config"
"github.com/charmbracelet/soft-serve/server/db"
Expand All @@ -18,7 +19,8 @@ import (
// StoreRepoMissingLFSObjects stores missing LFS objects for a repository.
func StoreRepoMissingLFSObjects(ctx context.Context, repo proto.Repository, dbx *db.DB, store store.Store, lfsClient lfs.Client) error {
cfg := config.FromContext(ctx)
lfsRoot := filepath.Join(cfg.DataPath, "lfs")
repoID := strconv.FormatInt(repo.ID(), 10)
lfsRoot := filepath.Join(cfg.DataPath, "lfs", repoID)

// TODO: support S3 storage
strg := storage.NewLocalStorage(lfsRoot)
Expand Down
112 changes: 89 additions & 23 deletions server/backend/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import (
"fmt"
"io/fs"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"time"

"github.com/charmbracelet/soft-serve/git"
Expand All @@ -28,7 +30,7 @@ func (d *Backend) reposPath() string {
// CreateRepository creates a new repository.
//
// It implements backend.Backend.
func (d *Backend) CreateRepository(ctx context.Context, name string, opts proto.RepositoryOptions) (proto.Repository, error) {
func (d *Backend) CreateRepository(ctx context.Context, name string, user proto.User, opts proto.RepositoryOptions) (proto.Repository, error) {
name = utils.SanitizeRepo(name)
if err := utils.ValidateRepo(name); err != nil {
return nil, err
Expand All @@ -37,11 +39,17 @@ func (d *Backend) CreateRepository(ctx context.Context, name string, opts proto.
repo := name + ".git"
rp := filepath.Join(d.reposPath(), repo)

var userID int64
if user != nil {
userID = user.ID()
}

if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
if err := d.store.CreateRepo(
ctx,
tx,
name,
userID,
opts.ProjectName,
opts.Description,
opts.Private,
Expand Down Expand Up @@ -72,14 +80,19 @@ func (d *Backend) CreateRepository(ctx context.Context, name string, opts proto.
return hooks.GenerateHooks(ctx, d.cfg, repo)
}); err != nil {
d.logger.Debug("failed to create repository in database", "err", err)
return nil, db.WrapError(err)
err = db.WrapError(err)
if errors.Is(err, db.ErrDuplicateKey) {
return nil, proto.ErrRepoExist
}

return nil, err
}

return d.Repository(ctx, name)
}

// ImportRepository imports a repository from remote.
func (d *Backend) ImportRepository(ctx context.Context, name string, remote string, opts proto.RepositoryOptions) (proto.Repository, error) {
func (d *Backend) ImportRepository(ctx context.Context, name string, user proto.User, remote string, opts proto.RepositoryOptions) (proto.Repository, error) {
name = utils.SanitizeRepo(name)
if err := utils.ValidateRepo(name); err != nil {
return nil, err
Expand All @@ -92,37 +105,42 @@ func (d *Backend) ImportRepository(ctx context.Context, name string, remote stri
return nil, proto.ErrRepoExist
}

copts := git.CloneOptions{
Bare: true,
Mirror: opts.Mirror,
Quiet: true,
CommandOptions: git.CommandOptions{
Timeout: -1,
Context: ctx,
Envs: []string{
fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile="%s" -o StrictHostKeyChecking=no -i "%s"`,
filepath.Join(d.cfg.DataPath, "ssh", "known_hosts"),
d.cfg.SSH.ClientKeyPath,
),
},
},
if err := os.MkdirAll(rp, fs.ModePerm); err != nil {
return nil, err
}

if err := git.Clone(remote, rp, copts); err != nil {
cmd := exec.CommandContext(ctx, "git", "clone", "--bare", "--mirror", remote, ".")
cmd.Env = append(cmd.Env,
fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile="%s" -o StrictHostKeyChecking=no -i "%s"`,
filepath.Join(d.cfg.DataPath, "ssh", "known_hosts"),
d.cfg.SSH.ClientKeyPath,
),
)
cmd.Dir = rp
if err := cmd.Run(); err != nil {
d.logger.Error("failed to clone repository", "err", err, "mirror", opts.Mirror, "remote", remote, "path", rp)
// Cleanup the mess!
if rerr := os.RemoveAll(rp); rerr != nil {
err = errors.Join(err, rerr)
}

return nil, err
}

r, err := d.CreateRepository(ctx, name, opts)
r, err := d.CreateRepository(ctx, name, user, opts)
if err != nil {
d.logger.Error("failed to create repository", "err", err, "name", name)
return nil, err
}

defer func() {
if err != nil {
if rerr := d.DeleteRepository(ctx, name, opts.LFS); rerr != nil {
d.logger.Error("failed to delete repository", "err", rerr, "name", name)
}
}
}()

rr, err := r.Open()
if err != nil {
d.logger.Error("failed to open repository", "err", err, "path", rp)
Expand All @@ -135,20 +153,28 @@ func (d *Backend) ImportRepository(ctx context.Context, name string, remote stri
return nil, err
}

rcfg.Section("lfs").SetOption("url", remote)
endpoint := remote
if opts.LFSEndpoint != "" {
endpoint = opts.LFSEndpoint
}

rcfg.Section("lfs").SetOption("url", endpoint)

if err := rr.SetConfig(rcfg); err != nil {
d.logger.Error("failed to set repository config", "err", err, "path", rp)
return nil, err
}

endpoint, err := lfs.NewEndpoint(remote)
ep, err := lfs.NewEndpoint(endpoint)
if err != nil {
d.logger.Error("failed to create lfs endpoint", "err", err, "path", rp)
return nil, err
}

client := lfs.NewClient(endpoint)
client := lfs.NewClient(ep)
if client == nil {
return nil, fmt.Errorf("failed to create lfs client: unsupported endpoint %s", endpoint)
}

if err := StoreRepoMissingLFSObjects(ctx, r, d.db, d.store, client); err != nil {
d.logger.Error("failed to store missing lfs objects", "err", err, "path", rp)
Expand All @@ -171,7 +197,13 @@ func (d *Backend) DeleteRepository(ctx context.Context, name string, deleteLFS b
defer d.cache.Delete(name)

if deleteLFS {
strg := storage.NewLocalStorage(filepath.Join(d.cfg.DataPath, "lfs"))
repom, err := d.store.GetRepoByName(ctx, tx, name)
if err != nil {
return err
}

repoID := strconv.FormatInt(repom.ID, 10)
strg := storage.NewLocalStorage(filepath.Join(d.cfg.DataPath, "lfs", repoID))
objs, err := d.store.GetLFSObjectsByName(ctx, tx, name)
if err != nil {
return err
Expand All @@ -198,6 +230,29 @@ func (d *Backend) DeleteRepository(ctx context.Context, name string, deleteLFS b
})
}

// DeleteUserRepositories deletes all user repositories.
func (d *Backend) DeleteUserRepositories(ctx context.Context, username string, deleteLFS bool) error {
return d.db.TransactionContext(ctx, func(tx *db.Tx) error {
user, err := d.store.FindUserByUsername(ctx, tx, username)
if err != nil {
return err
}

repos, err := d.store.GetUserRepos(ctx, tx, user.ID)
if err != nil {
return err
}

for _, repo := range repos {
if err := d.DeleteRepository(ctx, repo.Name, deleteLFS); err != nil {
return err
}
}

return nil
})
}

// RenameRepository renames a repository.
//
// It implements backend.Backend.
Expand Down Expand Up @@ -501,6 +556,17 @@ func (r *repo) ID() int64 {
return r.repo.ID
}

// UserID returns the repository's owner's user ID.
// If the repository is not owned by anyone, it returns 0.
//
// It implements proto.Repository.
func (r *repo) UserID() int64 {
if r.repo.UserID.Valid {
return r.repo.UserID.Int64
}
return 0
}

// Description returns the repository's description.
//
// It implements backend.Repository.
Expand Down
47 changes: 42 additions & 5 deletions server/backend/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ func (d *Backend) AccessLevelForUser(ctx context.Context, repo string, user prot
}

if r != nil {
if user != nil {
// If the user is the owner, they have admin access.
if r.UserID() == user.ID() {
return access.AdminAccess
}
}

// If the user is a collaborator, they have read/write access.
isCollab, _ := d.IsCollaborator(ctx, repo, username)
if isCollab {
Expand Down Expand Up @@ -128,6 +135,34 @@ func (d *Backend) User(ctx context.Context, username string) (proto.User, error)
}, nil
}

// UserByID finds a user by ID.
func (d *Backend) UserByID(ctx context.Context, id int64) (proto.User, error) {
var m models.User
var pks []ssh.PublicKey
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
m, err = d.store.GetUserByID(ctx, tx, id)
if err != nil {
return err
}

pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)
return err
}); err != nil {
err = db.WrapError(err)
if errors.Is(err, db.ErrRecordNotFound) {
return nil, proto.ErrUserNotFound
}
d.logger.Error("error finding user", "id", id, "error", err)
return nil, err
}

return &user{
user: m,
publicKeys: pks,
}, nil
}

// UserByPublicKey finds a user by public key.
//
// It implements backend.Backend.
Expand Down Expand Up @@ -263,11 +298,13 @@ func (d *Backend) DeleteUser(ctx context.Context, username string) error {
return err
}

return db.WrapError(
d.db.TransactionContext(ctx, func(tx *db.Tx) error {
return d.store.DeleteUserByUsername(ctx, tx, username)
}),
)
return d.db.TransactionContext(ctx, func(tx *db.Tx) error {
if err := d.store.DeleteUserByUsername(ctx, tx, username); err != nil {
return db.WrapError(err)
}

return d.DeleteUserRepositories(ctx, username)
})
}

// RemovePublicKey removes a public key from a user.
Expand Down
23 changes: 23 additions & 0 deletions server/db/migrate/0004_repo_owner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package migrate

import (
"context"

"github.com/charmbracelet/soft-serve/server/db"
)

const (
repoOwnerName = "repo owner"
repoOwnerVersion = 4
)

var repoOwner = Migration{
Version: repoOwnerVersion,
Name: repoOwnerName,
Migrate: func(ctx context.Context, tx *db.Tx) error {
return migrateUp(ctx, tx, repoOwnerVersion, repoOwnerName)
},
Rollback: func(ctx context.Context, tx *db.Tx) error {
return migrateDown(ctx, tx, repoOwnerVersion, repoOwnerName)
},
}
1 change: 1 addition & 0 deletions server/db/migrate/0004_repo_owner_postgres.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE repos DROP COLUMN user_id;
14 changes: 14 additions & 0 deletions server/db/migrate/0004_repo_owner_postgres.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
ALTER TABLE repos ADD COLUMN user_id INTEGER;

UPDATE repos SET user_id = (
SELECT id FROM users WHERE admin = true ORDER BY id LIMIT 1
);

ALTER TABLE repos
ALTER COLUMN user_id SET NOT NULL;

ALTER TABLE repos
ADD CONSTRAINT user_id_fk
FOREIGN KEY(user_id) REFERENCES users(id)
ON DELETE CASCADE
ON UPDATE CASCADE;
1 change: 1 addition & 0 deletions server/db/migrate/0004_repo_owner_sqlite.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS repos_old;
25 changes: 25 additions & 0 deletions server/db/migrate/0004_repo_owner_sqlite.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
ALTER TABLE repos RENAME TO repos_old;

CREATE TABLE repos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
project_name TEXT NOT NULL,
description TEXT NOT NULL,
private BOOLEAN NOT NULL,
mirror BOOLEAN NOT NULL,
hidden BOOLEAN NOT NULL,
user_id INTEGER NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL,
CONSTRAINT user_id_fk
FOREIGN KEY(user_id) REFERENCES users(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);

INSERT INTO repos (id, name, project_name, description, private, mirror, hidden, user_id, created_at, updated_at)
SELECT id, name, project_name, description, private, mirror, hidden, (
SELECT id FROM users WHERE admin = true ORDER BY id LIMIT 1
), created_at, updated_at
FROM repos_old;

1 change: 1 addition & 0 deletions server/db/migrate/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var migrations = []Migration{
createTables,
createLFSTables,
passwordTokens,
repoOwner,
}

func execMigration(ctx context.Context, tx *db.Tx, version int, name string, down bool) error {
Expand Down
Loading

0 comments on commit ea6b9a4

Please sign in to comment.