add support for user repos

This commit is contained in:
Massaki Archambault 2020-12-29 19:49:11 -05:00
parent 4aca123c98
commit 9b09b6aef9
10 changed files with 335 additions and 152 deletions

View File

@ -21,8 +21,8 @@ var _ = (fs.NodeReaddirer)((*groupNode)(nil))
// Ensure we are implementing the NodeLookuper interface
var _ = (fs.NodeLookuper)((*groupNode)(nil))
func newRootGroupNode(gid int, param *FSParam) (*groupNode, error) {
group, err := param.Gf.FetchGroup(gid)
func newGroupNodeByID(gid int, param *FSParam) (*groupNode, error) {
group, err := param.Gitlab.FetchGroup(gid)
if err != nil {
return nil, err
}
@ -42,19 +42,19 @@ func newGroupNode(group *gitlab.Group, param *FSParam) (*groupNode, error) {
}
func (n *groupNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
groupContent, _ := n.param.Gf.FetchGroupContent(n.group)
entries := make([]fuse.DirEntry, 0, len(groupContent.Groups)+len(groupContent.Repositories))
groupContent, _ := n.param.Gitlab.FetchGroupContent(n.group)
entries := make([]fuse.DirEntry, 0, len(groupContent.Groups)+len(groupContent.Projects))
for _, group := range groupContent.Groups {
entries = append(entries, fuse.DirEntry{
Name: group.Path,
Name: group.Name,
Ino: uint64(group.ID),
Mode: fuse.S_IFDIR,
})
}
for _, repository := range groupContent.Repositories {
for _, project := range groupContent.Projects {
entries = append(entries, fuse.DirEntry{
Name: repository.Path,
Ino: uint64(repository.ID),
Name: project.Name,
Ino: uint64(project.ID),
Mode: fuse.S_IFLNK,
})
}
@ -62,7 +62,7 @@ func (n *groupNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
}
func (n *groupNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
groupContent, _ := n.param.Gf.FetchGroupContent(n.group)
groupContent, _ := n.param.Gitlab.FetchGroupContent(n.group)
// Check if the map of groups contains it
group, ok := groupContent.Groups[name]
@ -75,14 +75,14 @@ func (n *groupNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut)
return n.NewInode(ctx, groupNode, attrs), 0
}
// Check if the map of repositories contains it
repository, ok := groupContent.Repositories[name]
// Check if the map of projects contains it
project, ok := groupContent.Projects[name]
if ok {
attrs := fs.StableAttr{
Ino: uint64(repository.ID),
Ino: uint64(project.ID),
Mode: fuse.S_IFLNK,
}
repositoryNode, _ := newRepositoryNode(repository, n.param)
repositoryNode, _ := newRepositoryNode(project, n.param)
return n.NewInode(ctx, repositoryNode, attrs), 0
}
return nil, syscall.ENOENT

View File

@ -18,7 +18,7 @@ type projectsNode struct {
// Ensure we are implementing the NodeOnAdder interface
var _ = (fs.NodeOnAdder)((*projectsNode)(nil))
func NewProjectsNode(rootGroupIds []int, param *FSParam) *projectsNode {
func newProjectsNode(rootGroupIds []int, param *FSParam) *projectsNode {
return &projectsNode{
param: param,
rootGroupIds: rootGroupIds,
@ -27,7 +27,7 @@ func NewProjectsNode(rootGroupIds []int, param *FSParam) *projectsNode {
func (n *projectsNode) OnAdd(ctx context.Context) {
for _, groupID := range n.rootGroupIds {
groupNode, err := newRootGroupNode(groupID, n.param)
groupNode, err := newGroupNodeByID(groupID, n.param)
if err != nil {
fmt.Printf("root group fetch fail: %v\n", err)
}
@ -39,6 +39,6 @@ func (n *projectsNode) OnAdd(ctx context.Context) {
Mode: fuse.S_IFDIR,
},
)
n.AddChild(groupNode.group.Path, inode, false)
n.AddChild(groupNode.group.Name, inode, false)
}
}

View File

@ -11,17 +11,17 @@ import (
type RepositoryNode struct {
fs.Inode
param *FSParam
repository *gitlab.Repository
project *gitlab.Project
}
// Ensure we are implementing the NodeReaddirer interface
var _ = (fs.NodeReadlinker)((*RepositoryNode)(nil))
func newRepositoryNode(repository *gitlab.Repository, param *FSParam) (*RepositoryNode, error) {
func newRepositoryNode(project *gitlab.Project, param *FSParam) (*RepositoryNode, error) {
node := &RepositoryNode{
param: param,
repository: repository,
project: project,
}
// Passthrough the error if there is one, nothing to add here
// Errors on clone/pull are non-fatal
@ -30,7 +30,7 @@ func newRepositoryNode(repository *gitlab.Repository, param *FSParam) (*Reposito
func (n *RepositoryNode) Readlink(ctx context.Context) ([]byte, syscall.Errno) {
// Create the local copy of the repo
localRepoLoc, _ := n.param.Gcp.CloneOrPull(n.repository.CloneURL, n.repository.ID, "master")
localRepoLoc, _ := n.param.Git.CloneOrPull(n.project.CloneURL, n.project.ID, "master")
return []byte(localRepoLoc), 0
}

View File

@ -16,8 +16,8 @@ const (
)
type FSParam struct {
Gf gitlab.GroupFetcher
Gcp git.GitClonerPuller
Gitlab gitlab.GitlabFetcher
Git git.GitClonerPuller
staticInoChan chan uint64
}
@ -34,7 +34,7 @@ var _ = (fs.NodeOnAdder)((*rootNode)(nil))
func (n *rootNode) OnAdd(ctx context.Context) {
projectsInode := n.NewPersistentInode(
ctx,
NewProjectsNode(
newProjectsNode(
n.rootGroupIds,
n.param,
),
@ -47,7 +47,7 @@ func (n *rootNode) OnAdd(ctx context.Context) {
usersInode := n.NewPersistentInode(
ctx,
NewUsersNode(
newUsersNode(
n.userIds,
n.param,
),

View File

@ -2,8 +2,12 @@ package fs
import (
"context"
"fmt"
"syscall"
"github.com/badjware/gitlabfs/gitlab"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
)
type usersNode struct {
@ -16,7 +20,7 @@ type usersNode struct {
// Ensure we are implementing the NodeOnAdder interface
var _ = (fs.NodeOnAdder)((*usersNode)(nil))
func NewUsersNode(userIds []int, param *FSParam) *usersNode {
func newUsersNode(userIds []int, param *FSParam) *usersNode {
return &usersNode{
param: param,
userIds: userIds,
@ -24,19 +28,101 @@ func NewUsersNode(userIds []int, param *FSParam) *usersNode {
}
func (n *usersNode) OnAdd(ctx context.Context) {
// for _, userId := range n.userIds {
// userNode, err := newRootUserNode(userId, n.param)
// if err != nil {
// fmt.Printf("user fetch fail: %v\n", err)
// }
// inode := n.NewPersistentInode(
// ctx,
// userNode,
// fs.StableAttr{
// Ino: <-n.param.staticInoChan,
// Mode: fuse.S_IFDIR,
// },
// )
// n.AddChild(userNode.user.Path, inode, false)
// }
// Fetch the current logged user
currentUser, err := n.param.Gitlab.FetchCurrentUser()
// Skip if we are anonymous (or the call fails for some reason...)
if err != nil {
fmt.Println(err)
} else {
currentUserNode, _ := newUserNode(currentUser, n.param)
inode := n.NewPersistentInode(
ctx,
currentUserNode,
fs.StableAttr{
Ino: <-n.param.staticInoChan,
Mode: fuse.S_IFDIR,
},
)
n.AddChild(currentUserNode.user.Name, inode, false)
}
for _, userID := range n.userIds {
if currentUser != nil && currentUser.ID == userID {
// We already added the current user, we can skip it
continue
}
userNode, err := newUserNodeByID(userID, n.param)
if err != nil {
fmt.Printf("user fetch fail: %v\n", err)
}
inode := n.NewPersistentInode(
ctx,
userNode,
fs.StableAttr{
Ino: <-n.param.staticInoChan,
Mode: fuse.S_IFDIR,
},
)
n.AddChild(userNode.user.Name, inode, false)
}
}
type userNode struct {
fs.Inode
param *FSParam
user *gitlab.User
}
// Ensure we are implementing the NodeReaddirer interface
var _ = (fs.NodeReaddirer)((*userNode)(nil))
// Ensure we are implementing the NodeLookuper interface
var _ = (fs.NodeLookuper)((*userNode)(nil))
func newUserNodeByID(uid int, param *FSParam) (*userNode, error) {
user, err := param.Gitlab.FetchUser(uid)
if err != nil {
return nil, err
}
node := &userNode{
param: param,
user: user,
}
return node, nil
}
func newUserNode(user *gitlab.User, param *FSParam) (*userNode, error) {
node := &userNode{
param: param,
user: user,
}
return node, nil
}
func (n *userNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
userContent, _ := n.param.Gitlab.FetchUserContent(n.user)
entries := make([]fuse.DirEntry, 0, len(userContent.Projects))
for _, project := range userContent.Projects {
entries = append(entries, fuse.DirEntry{
Name: project.Name,
Ino: uint64(project.ID),
Mode: fuse.S_IFLNK,
})
}
return fs.NewListDirStream(entries), 0
}
func (n *userNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
userContent, _ := n.param.Gitlab.FetchUserContent(n.user)
project, ok := userContent.Projects[name]
if ok {
attrs := fs.StableAttr{
Ino: uint64(project.ID),
Mode: fuse.S_IFLNK,
}
repositoryNode, _ := newRepositoryNode(project, n.param)
return n.NewInode(ctx, repositoryNode, attrs), 0
}
return nil, syscall.ENOENT
}

View File

@ -6,28 +6,9 @@ import (
"github.com/xanzy/go-gitlab"
)
type GroupFetcher interface {
FetchGroup(gid int) (*Group, error)
FetchGroupContent(group *Group) (*GroupContent, error)
}
type GroupContent struct {
Groups map[string]*Group
Repositories map[string]*Repository
}
type Group struct {
ID int
Name string
Path string
Content *GroupContent
}
type Repository struct {
ID int
Name string
Path string
CloneURL string
type GitlabFetcher interface {
GroupFetcher
UserFetcher
}
type GitlabClientParam struct {
@ -52,90 +33,3 @@ func NewClient(gitlabUrl string, gitlabToken string, p GitlabClientParam) (*gitl
}
return gitlabClient, nil
}
func NewRepositoryFromGitlabProject(project *gitlab.Project) Repository {
// https://godoc.org/github.com/xanzy/go-gitlab#Project
return Repository{
ID: project.ID,
Name: project.Name,
Path: project.Path,
CloneURL: project.HTTPURLToRepo,
// CloneUrl: project.SSHURLToRepo,
}
}
func NewGroupFromGitlabGroup(group *gitlab.Group) Group {
// https://godoc.org/github.com/xanzy/go-gitlab#Group
return Group{
ID: group.ID,
Name: group.Name,
Path: group.Path,
}
}
func (c gitlabClient) FetchGroup(gid int) (*Group, error) {
gitlabGroup, _, err := c.client.Groups.GetGroup(gid)
if err != nil {
return nil, fmt.Errorf("failed to fetch group with id %v: %v\n", gid, err)
}
group := NewGroupFromGitlabGroup(gitlabGroup)
return &group, nil
}
func (c gitlabClient) FetchGroupContent(group *Group) (*GroupContent, error) {
if group.Content != nil {
return group.Content, nil
}
content := &GroupContent{
Groups: map[string]*Group{},
Repositories: map[string]*Repository{},
}
// List subgroups in path
ListGroupsOpt := &gitlab.ListSubgroupsOptions{
ListOptions: gitlab.ListOptions{
Page: 1,
PerPage: 1000,
}}
for {
gitlabGroups, response, err := c.client.Groups.ListSubgroups(group.ID, ListGroupsOpt)
if err != nil {
return nil, fmt.Errorf("failed to fetch groups in gitlab: %v", err)
}
for _, gitlabGroup := range gitlabGroups {
group := NewGroupFromGitlabGroup(gitlabGroup)
content.Groups[group.Path] = &group
}
if response.CurrentPage >= response.TotalPages {
break
}
// Get the next page
ListGroupsOpt.Page = response.NextPage
}
// List repositories in path
listProjectOpt := &gitlab.ListGroupProjectsOptions{
ListOptions: gitlab.ListOptions{
Page: 1,
PerPage: 1000,
}}
for {
gitlabProjects, response, err := c.client.Groups.ListGroupProjects(group.ID, listProjectOpt)
if err != nil {
return nil, fmt.Errorf("failed to fetch projects in gitlab: %v", err)
}
for _, gitlabProject := range gitlabProjects {
repository := NewRepositoryFromGitlabProject(gitlabProject)
content.Repositories[repository.Path] = &repository
}
if response.CurrentPage >= response.TotalPages {
break
}
// Get the next page
listProjectOpt.Page = response.NextPage
}
group.Content = content
return content, nil
}

99
gitlab/group.go Normal file
View File

@ -0,0 +1,99 @@
package gitlab
import (
"fmt"
"github.com/xanzy/go-gitlab"
)
type GroupFetcher interface {
FetchGroup(gid int) (*Group, error)
FetchGroupContent(group *Group) (*GroupContent, error)
}
type GroupContent struct {
Groups map[string]*Group
Projects map[string]*Project
}
type Group struct {
ID int
Name string
content *GroupContent
}
func NewGroupFromGitlabGroup(group *gitlab.Group) Group {
// https://godoc.org/github.com/xanzy/go-gitlab#Group
return Group{
ID: group.ID,
Name: group.Path,
}
}
func (c *gitlabClient) FetchGroup(gid int) (*Group, error) {
gitlabGroup, _, err := c.client.Groups.GetGroup(gid)
if err != nil {
return nil, fmt.Errorf("failed to fetch group with id %v: %v", gid, err)
}
group := NewGroupFromGitlabGroup(gitlabGroup)
return &group, nil
}
func (c *gitlabClient) FetchGroupContent(group *Group) (*GroupContent, error) {
if group.content != nil {
return group.content, nil
}
content := &GroupContent{
Groups: map[string]*Group{},
Projects: map[string]*Project{},
}
// List subgroups in path
ListGroupsOpt := &gitlab.ListSubgroupsOptions{
ListOptions: gitlab.ListOptions{
Page: 1,
PerPage: 1000,
}}
for {
gitlabGroups, response, err := c.client.Groups.ListSubgroups(group.ID, ListGroupsOpt)
if err != nil {
return nil, fmt.Errorf("failed to fetch groups in gitlab: %v", err)
}
for _, gitlabGroup := range gitlabGroups {
group := NewGroupFromGitlabGroup(gitlabGroup)
content.Groups[group.Name] = &group
}
if response.CurrentPage >= response.TotalPages {
break
}
// Get the next page
ListGroupsOpt.Page = response.NextPage
}
// List projects in path
listProjectOpt := &gitlab.ListGroupProjectsOptions{
ListOptions: gitlab.ListOptions{
Page: 1,
PerPage: 1000,
}}
for {
gitlabProjects, response, err := c.client.Groups.ListGroupProjects(group.ID, listProjectOpt)
if err != nil {
return nil, fmt.Errorf("failed to fetch projects in gitlab: %v", err)
}
for _, gitlabProject := range gitlabProjects {
project := NewProjectFromGitlabProject(gitlabProject)
content.Projects[project.Name] = &project
}
if response.CurrentPage >= response.TotalPages {
break
}
// Get the next page
listProjectOpt.Page = response.NextPage
}
group.content = content
return content, nil
}

19
gitlab/project.go Normal file
View File

@ -0,0 +1,19 @@
package gitlab
import "github.com/xanzy/go-gitlab"
type Project struct {
ID int
Name string
CloneURL string
}
func NewProjectFromGitlabProject(project *gitlab.Project) Project {
// https://godoc.org/github.com/xanzy/go-gitlab#Project
return Project{
ID: project.ID,
Name: project.Path,
// CloneURL: project.HTTPURLToRepo,
CloneURL: project.SSHURLToRepo,
}
}

85
gitlab/user.go Normal file
View File

@ -0,0 +1,85 @@
package gitlab
import (
"fmt"
"github.com/xanzy/go-gitlab"
)
type UserFetcher interface {
FetchUser(uid int) (*User, error)
FetchCurrentUser() (*User, error)
FetchUserContent(user *User) (*UserContent, error)
}
type UserContent struct {
Projects map[string]*Project
}
type User struct {
ID int
Name string
content *UserContent
}
func NewUserFromGitlabUser(user *gitlab.User) User {
// https://godoc.org/github.com/xanzy/go-gitlab#User
return User{
ID: user.ID,
Name: user.Username,
}
}
func (c *gitlabClient) FetchUser(uid int) (*User, error) {
gitlabUser, _, err := c.client.Users.GetUser(uid)
if err != nil {
return nil, fmt.Errorf("failed to fetch user with id %v: %v", uid, err)
}
user := NewUserFromGitlabUser(gitlabUser)
return &user, nil
}
func (c *gitlabClient) FetchCurrentUser() (*User, error) {
gitlabUser, _, err := c.client.Users.CurrentUser()
if err != nil {
return nil, fmt.Errorf("failed to fetch current user: %v", err)
}
user := NewUserFromGitlabUser(gitlabUser)
return &user, nil
}
func (c *gitlabClient) FetchUserContent(user *User) (*UserContent, error) {
if user.content != nil {
return user.content, nil
}
content := &UserContent{
Projects: map[string]*Project{},
}
// Fetch the user repositories
listProjectOpt := &gitlab.ListProjectsOptions{
ListOptions: gitlab.ListOptions{
Page: 1,
PerPage: 1000,
}}
for {
gitlabProjects, response, err := c.client.Projects.ListUserProjects(user.ID, listProjectOpt)
if err != nil {
return nil, fmt.Errorf("failed to fetch projects in gitlab: %v", err)
}
for _, gitlabProject := range gitlabProjects {
project := NewProjectFromGitlabProject(gitlabProject)
content.Projects[project.Name] = &project
}
if response.CurrentPage >= response.TotalPages {
break
}
// Get the next page
listProjectOpt.Page = response.NextPage
}
user.content = content
return content, nil
}

View File

@ -45,5 +45,5 @@ func main() {
gitClient, _ := git.NewClient(gitClientParam)
// Start the filesystem
fs.Start(mountpoint, []int{*gitlabRootGroupID}, []int{}, &fs.FSParam{Gf: gitlabClient, Gcp: gitClient})
fs.Start(mountpoint, []int{*gitlabRootGroupID}, []int{}, &fs.FSParam{Gitlab: gitlabClient, Git: gitClient})
}