diff --git a/fs/group.go b/fs/group.go index a903293..3d75a50 100644 --- a/fs/group.go +++ b/fs/group.go @@ -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 diff --git a/fs/projects.go b/fs/projects.go index 05f1064..777efe8 100644 --- a/fs/projects.go +++ b/fs/projects.go @@ -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) } } diff --git a/fs/repository.go b/fs/repository.go index d9497e4..a614bf6 100644 --- a/fs/repository.go +++ b/fs/repository.go @@ -10,18 +10,18 @@ import ( type RepositoryNode struct { fs.Inode - param *FSParam - repository *gitlab.Repository + param *FSParam + 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, + param: param, + 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 } diff --git a/fs/root.go b/fs/root.go index e11c9c3..8d2f94c 100644 --- a/fs/root.go +++ b/fs/root.go @@ -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, ), diff --git a/fs/users.go b/fs/users.go index f247be8..0bdcabe 100644 --- a/fs/users.go +++ b/fs/users.go @@ -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 } diff --git a/gitlab/client.go b/gitlab/client.go index 93b19a3..11c8f3e 100644 --- a/gitlab/client.go +++ b/gitlab/client.go @@ -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 -} diff --git a/gitlab/group.go b/gitlab/group.go new file mode 100644 index 0000000..a20c558 --- /dev/null +++ b/gitlab/group.go @@ -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 +} diff --git a/gitlab/project.go b/gitlab/project.go new file mode 100644 index 0000000..695f409 --- /dev/null +++ b/gitlab/project.go @@ -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, + } +} diff --git a/gitlab/user.go b/gitlab/user.go new file mode 100644 index 0000000..dd8566c --- /dev/null +++ b/gitlab/user.go @@ -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 +} diff --git a/main.go b/main.go index c7145ae..14a2cc5 100644 --- a/main.go +++ b/main.go @@ -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}) }