refactor to decouple fs package from gitlab package

This commit is contained in:
Massaki Archambault 2024-05-05 16:09:03 -04:00
parent 026089d786
commit 5ed64d523e
13 changed files with 357 additions and 482 deletions

View File

@ -2,9 +2,9 @@ package fs
import ( import (
"context" "context"
"fmt"
"syscall" "syscall"
"github.com/badjware/gitlabfs/gitlab"
"github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse"
) )
@ -13,56 +13,50 @@ type groupNode struct {
fs.Inode fs.Inode
param *FSParam param *FSParam
group *gitlab.Group source GroupSource
staticNodes map[string]staticNode staticNodes map[string]staticNode
} }
type GroupSource interface {
GetGroupID() uint64
InvalidateCache()
}
// Ensure we are implementing the NodeReaddirer interface // Ensure we are implementing the NodeReaddirer interface
var _ = (fs.NodeReaddirer)((*groupNode)(nil)) var _ = (fs.NodeReaddirer)((*groupNode)(nil))
// Ensure we are implementing the NodeLookuper interface // Ensure we are implementing the NodeLookuper interface
var _ = (fs.NodeLookuper)((*groupNode)(nil)) var _ = (fs.NodeLookuper)((*groupNode)(nil))
func newGroupNodeByID(gid int, param *FSParam) (*groupNode, error) { func newGroupNodeFromSource(source GroupSource, param *FSParam) (*groupNode, error) {
group, err := param.Gitlab.FetchGroup(gid)
if err != nil {
return nil, err
}
node := &groupNode{ node := &groupNode{
param: param, param: param,
group: group, source: source,
staticNodes: map[string]staticNode{ staticNodes: map[string]staticNode{
".refresh": newRefreshNode(group, param), ".refresh": newRefreshNode(source, param),
},
}
return node, nil
}
func newGroupNode(group *gitlab.Group, param *FSParam) (*groupNode, error) {
node := &groupNode{
param: param,
group: group,
staticNodes: map[string]staticNode{
".refresh": newRefreshNode(group, param),
}, },
} }
return node, nil return node, nil
} }
func (n *groupNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { func (n *groupNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
groupContent, _ := n.param.Gitlab.FetchGroupContent(n.group) groups, repositories, err := n.param.GitPlatform.FetchGroupContent(n.source.GetGroupID())
entries := make([]fuse.DirEntry, 0, len(groupContent.Groups)+len(groupContent.Projects)+len(n.staticNodes)) if err != nil {
for _, group := range groupContent.Groups { fmt.Errorf("%v", err)
}
entries := make([]fuse.DirEntry, 0, len(groups)+len(repositories)+len(n.staticNodes))
for groupName, group := range groups {
entries = append(entries, fuse.DirEntry{ entries = append(entries, fuse.DirEntry{
Name: group.Name, Name: groupName,
Ino: uint64(group.ID), Ino: group.GetGroupID(),
Mode: fuse.S_IFDIR, Mode: fuse.S_IFDIR,
}) })
} }
for _, project := range groupContent.Projects { for repositoryName, repository := range repositories {
entries = append(entries, fuse.DirEntry{ entries = append(entries, fuse.DirEntry{
Name: project.Name, Name: repositoryName,
Ino: uint64(project.ID), Ino: repository.GetRepositoryID(),
Mode: fuse.S_IFLNK, Mode: fuse.S_IFLNK,
}) })
} }
@ -77,27 +71,27 @@ 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) { func (n *groupNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
groupContent, _ := n.param.Gitlab.FetchGroupContent(n.group) groups, repositories, _ := n.param.GitPlatform.FetchGroupContent(n.source.GetGroupID())
// Check if the map of groups contains it // Check if the map of groups contains it
group, ok := groupContent.Groups[name] group, found := groups[name]
if ok { if found {
attrs := fs.StableAttr{ attrs := fs.StableAttr{
Ino: uint64(group.ID), Ino: group.GetGroupID(),
Mode: fuse.S_IFDIR, Mode: fuse.S_IFDIR,
} }
groupNode, _ := newGroupNode(group, n.param) groupNode, _ := newGroupNodeFromSource(group, n.param)
return n.NewInode(ctx, groupNode, attrs), 0 return n.NewInode(ctx, groupNode, attrs), 0
} }
// Check if the map of projects contains it // Check if the map of projects contains it
project, ok := groupContent.Projects[name] repository, found := repositories[name]
if ok { if found {
attrs := fs.StableAttr{ attrs := fs.StableAttr{
Ino: uint64(project.ID), Ino: repository.GetRepositoryID(),
Mode: fuse.S_IFLNK, Mode: fuse.S_IFLNK,
} }
repositoryNode, _ := newRepositoryNode(project, n.param) repositoryNode, _ := newRepositoryNodeFromSource(repository, n.param)
return n.NewInode(ctx, repositoryNode, attrs), 0 return n.NewInode(ctx, repositoryNode, attrs), 0
} }

View File

@ -1,47 +0,0 @@
package fs
import (
"context"
"fmt"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
)
type groupsNode struct {
fs.Inode
param *FSParam
rootGroupIds []int
}
// Ensure we are implementing the NodeOnAdder interface
var _ = (fs.NodeOnAdder)((*groupsNode)(nil))
func newGroupsNode(rootGroupIds []int, param *FSParam) *groupsNode {
return &groupsNode{
param: param,
rootGroupIds: rootGroupIds,
}
}
func (n *groupsNode) OnAdd(ctx context.Context) {
for _, groupID := range n.rootGroupIds {
groupNode, err := newGroupNodeByID(groupID, n.param)
if err != nil {
fmt.Printf("root group fetch fail: %v\n", err)
fmt.Printf("Please verify the group exists, is public or a token with sufficient permissions is set in the config files.\n")
fmt.Printf("Skipping group %v\n", groupID)
return
}
inode := n.NewPersistentInode(
ctx,
groupNode,
fs.StableAttr{
Ino: <-n.param.staticInoChan,
Mode: fuse.S_IFDIR,
},
)
n.AddChild(groupNode.group.Name, inode, false)
}
}

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"syscall" "syscall"
"github.com/badjware/gitlabfs/gitlab"
"github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse"
) )
@ -12,7 +11,8 @@ import (
type refreshNode struct { type refreshNode struct {
fs.Inode fs.Inode
ino uint64 ino uint64
refresher gitlab.Refresher
source GroupSource
} }
// Ensure we are implementing the NodeSetattrer interface // Ensure we are implementing the NodeSetattrer interface
@ -21,10 +21,10 @@ var _ = (fs.NodeSetattrer)((*refreshNode)(nil))
// Ensure we are implementing the NodeOpener interface // Ensure we are implementing the NodeOpener interface
var _ = (fs.NodeOpener)((*refreshNode)(nil)) var _ = (fs.NodeOpener)((*refreshNode)(nil))
func newRefreshNode(refresher gitlab.Refresher, param *FSParam) *refreshNode { func newRefreshNode(source GroupSource, param *FSParam) *refreshNode {
return &refreshNode{ return &refreshNode{
ino: <-param.staticInoChan, ino: <-param.staticInoChan,
refresher: refresher, source: source,
} }
} }
@ -41,6 +41,6 @@ func (n *refreshNode) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.Se
} }
func (n *refreshNode) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { func (n *refreshNode) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
n.refresher.InvalidateCache() n.source.InvalidateCache()
return nil, 0, 0 return nil, 0, 0
} }

View File

@ -4,23 +4,30 @@ import (
"context" "context"
"syscall" "syscall"
"github.com/badjware/gitlabfs/gitlab"
"github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fs"
) )
type RepositoryNode struct { type RepositoryNode struct {
fs.Inode fs.Inode
param *FSParam param *FSParam
project *gitlab.Project
source RepositorySource
}
type RepositorySource interface {
// GetName() string
GetRepositoryID() uint64
GetCloneURL() string
GetDefaultBranch() string
} }
// Ensure we are implementing the NodeReaddirer interface // Ensure we are implementing the NodeReaddirer interface
var _ = (fs.NodeReadlinker)((*RepositoryNode)(nil)) var _ = (fs.NodeReadlinker)((*RepositoryNode)(nil))
func newRepositoryNode(project *gitlab.Project, param *FSParam) (*RepositoryNode, error) { func newRepositoryNodeFromSource(source RepositorySource, param *FSParam) (*RepositoryNode, error) {
node := &RepositoryNode{ node := &RepositoryNode{
param: param, param: param,
project: project, source: source,
} }
// Passthrough the error if there is one, nothing to add here // Passthrough the error if there is one, nothing to add here
// Errors on clone/pull are non-fatal // Errors on clone/pull are non-fatal
@ -29,7 +36,8 @@ func newRepositoryNode(project *gitlab.Project, param *FSParam) (*RepositoryNode
func (n *RepositoryNode) Readlink(ctx context.Context) ([]byte, syscall.Errno) { func (n *RepositoryNode) Readlink(ctx context.Context) ([]byte, syscall.Errno) {
// Create the local copy of the repo // Create the local copy of the repo
localRepoLoc, _ := n.param.Git.CloneOrPull(n.project.CloneURL, n.project.ID, n.project.DefaultBranch) // TODO: cleanup
localRepositoryPath, _ := n.param.GitImplementation.CloneOrPull(n.source.GetCloneURL(), int(n.source.GetRepositoryID()), n.source.GetDefaultBranch())
return []byte(localRepoLoc), 0 return []byte(localRepositoryPath), 0
} }

View File

@ -8,8 +8,6 @@ import (
"syscall" "syscall"
"github.com/badjware/gitlabfs/git" "github.com/badjware/gitlabfs/git"
"github.com/badjware/gitlabfs/gitlab"
"github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse"
) )
@ -24,12 +22,14 @@ type staticNode interface {
Mode() uint32 Mode() uint32
} }
type FSParam struct { type GitPlatform interface {
Git git.GitClonerPuller FetchRootGroupContent() (map[string]GroupSource, error)
Gitlab gitlab.GitlabFetcher FetchGroupContent(gid uint64) (map[string]GroupSource, map[string]RepositorySource, error)
}
RootGroupIds []int type FSParam struct {
UserIds []int GitImplementation git.GitClonerPuller
GitPlatform GitPlatform
staticInoChan chan uint64 staticInoChan chan uint64
} }
@ -37,42 +37,10 @@ type FSParam struct {
type rootNode struct { type rootNode struct {
fs.Inode fs.Inode
param *FSParam param *FSParam
rootGroupIds []int
userIds []int
} }
var _ = (fs.NodeOnAdder)((*rootNode)(nil)) var _ = (fs.NodeOnAdder)((*rootNode)(nil))
func (n *rootNode) OnAdd(ctx context.Context) {
groupsInode := n.NewPersistentInode(
ctx,
newGroupsNode(
n.rootGroupIds,
n.param,
),
fs.StableAttr{
Ino: <-n.param.staticInoChan,
Mode: fuse.S_IFDIR,
},
)
n.AddChild("groups", groupsInode, false)
usersInode := n.NewPersistentInode(
ctx,
newUsersNode(
n.userIds,
n.param,
),
fs.StableAttr{
Ino: <-n.param.staticInoChan,
Mode: fuse.S_IFDIR,
},
)
n.AddChild("users", usersInode, false)
fmt.Println("Mounted and ready to use")
}
func Start(mountpoint string, mountoptions []string, param *FSParam, debug bool) error { func Start(mountpoint string, mountoptions []string, param *FSParam, debug bool) error {
fmt.Printf("Mounting in %v\n", mountpoint) fmt.Printf("Mounting in %v\n", mountpoint)
@ -83,8 +51,6 @@ func Start(mountpoint string, mountoptions []string, param *FSParam, debug bool)
param.staticInoChan = make(chan uint64) param.staticInoChan = make(chan uint64)
root := &rootNode{ root := &rootNode{
param: param, param: param,
rootGroupIds: param.RootGroupIds,
userIds: param.UserIds,
} }
go staticInoGenerator(root.param.staticInoChan) go staticInoGenerator(root.param.staticInoChan)
@ -104,6 +70,28 @@ func Start(mountpoint string, mountoptions []string, param *FSParam, debug bool)
return nil return nil
} }
func (n *rootNode) OnAdd(ctx context.Context) {
rootGroups, err := n.param.GitPlatform.FetchRootGroupContent()
if err != nil {
panic(err)
}
for groupName, group := range rootGroups {
groupNode, _ := newGroupNodeFromSource(group, n.param)
persistentInode := n.NewPersistentInode(
ctx,
groupNode,
fs.StableAttr{
Ino: <-n.param.staticInoChan,
Mode: fuse.S_IFDIR,
},
)
n.AddChild(groupName, persistentInode, false)
}
fmt.Println("Mounted and ready to use")
}
func staticInoGenerator(staticInoChan chan<- uint64) { func staticInoGenerator(staticInoChan chan<- uint64) {
i := staticInodeStart i := staticInodeStart
for { for {

View File

@ -1,159 +0,0 @@
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 {
fs.Inode
param *FSParam
userIds []int
}
// Ensure we are implementing the NodeOnAdder interface
var _ = (fs.NodeOnAdder)((*usersNode)(nil))
func newUsersNode(userIds []int, param *FSParam) *usersNode {
return &usersNode{
param: param,
userIds: userIds,
}
}
func (n *usersNode) OnAdd(ctx context.Context) {
// 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)
fmt.Printf("Please verify the user exists and token with sufficient permissions is set in the config files.\n")
fmt.Printf("Skipping user %v\n", userID)
return
}
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
staticNodes map[string]staticNode
}
// 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,
staticNodes: map[string]staticNode{
".refresh": newRefreshNode(user, param),
},
}
return node, nil
}
func newUserNode(user *gitlab.User, param *FSParam) (*userNode, error) {
node := &userNode{
param: param,
user: user,
staticNodes: map[string]staticNode{
".refresh": newRefreshNode(user, param),
},
}
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)+len(n.staticNodes))
for _, project := range userContent.Projects {
entries = append(entries, fuse.DirEntry{
Name: project.Name,
Ino: uint64(project.ID),
Mode: fuse.S_IFLNK,
})
}
for name, staticNode := range n.staticNodes {
entries = append(entries, fuse.DirEntry{
Name: name,
Ino: staticNode.Ino(),
Mode: staticNode.Mode(),
})
}
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)
// Check if the map of projects contains it
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
}
// Check if the map of static nodes contains it
staticNode, ok := n.staticNodes[name]
if ok {
attrs := fs.StableAttr{
Ino: staticNode.Ino(),
Mode: staticNode.Mode(),
}
return n.NewInode(ctx, staticNode, attrs), 0
}
return nil, syscall.ENOENT
}

View File

@ -2,7 +2,9 @@ package gitlab
import ( import (
"fmt" "fmt"
"slices"
"github.com/badjware/gitlabfs/fs"
"github.com/xanzy/go-gitlab" "github.com/xanzy/go-gitlab"
) )
@ -11,26 +13,29 @@ const (
PullMethodSSH = "ssh" PullMethodSSH = "ssh"
) )
type GitlabFetcher interface { type GitlabClientConfig struct {
GroupFetcher URL string `yaml:"url,omitempty"`
UserFetcher Token string `yaml:"token,omitempty"`
} GroupIDs []int `yaml:"group_ids,omitempty"`
UserIDs []int `yaml:"user_ids,omitempty"`
type Refresher interface { IncludeCurrentUser bool `yaml:"include_current_user,omitempty"`
InvalidateCache() PullMethod string `yaml:"pull_method,omitempty"`
}
type GitlabClientParam struct {
PullMethod string
IncludeCurrentUser bool
} }
type gitlabClient struct { type gitlabClient struct {
GitlabClientParam GitlabClientConfig
client *gitlab.Client client *gitlab.Client
// root group cache
rootGroupCache map[string]fs.GroupSource
currentUserCache *User
// API response cache
groupCache map[int]*Group
userCache map[int]*User
} }
func NewClient(gitlabUrl string, gitlabToken string, p GitlabClientParam) (*gitlabClient, error) { func NewClient(gitlabUrl string, gitlabToken string, p GitlabClientConfig) (*gitlabClient, error) {
client, err := gitlab.NewClient( client, err := gitlab.NewClient(
gitlabToken, gitlabToken,
gitlab.WithBaseURL(gitlabUrl), gitlab.WithBaseURL(gitlabUrl),
@ -40,8 +45,67 @@ func NewClient(gitlabUrl string, gitlabToken string, p GitlabClientParam) (*gitl
} }
gitlabClient := &gitlabClient{ gitlabClient := &gitlabClient{
GitlabClientParam: p, GitlabClientConfig: p,
client: client, client: client,
rootGroupCache: nil,
currentUserCache: nil,
groupCache: map[int]*Group{},
userCache: map[int]*User{},
} }
return gitlabClient, nil return gitlabClient, nil
} }
func (c *gitlabClient) FetchRootGroupContent() (map[string]fs.GroupSource, error) {
// use cached values if available
if c.rootGroupCache == nil {
rootGroupCache := make(map[string]fs.GroupSource)
// fetch root groups
for _, gid := range c.GroupIDs {
group, err := c.fetchGroup(gid)
if err != nil {
return nil, err
}
rootGroupCache[group.Name] = group
}
// fetch users
for _, uid := range c.UserIDs {
user, err := c.fetchUser(uid)
if err != nil {
return nil, err
}
rootGroupCache[user.Name] = user
}
// fetch current user if configured
if c.IncludeCurrentUser {
currentUser, err := c.fetchCurrentUser()
if err != nil {
return nil, err
}
rootGroupCache[currentUser.Name] = currentUser
}
c.rootGroupCache = rootGroupCache
}
return c.rootGroupCache, nil
}
func (c *gitlabClient) FetchGroupContent(gid uint64) (map[string]fs.GroupSource, map[string]fs.RepositorySource, error) {
if slices.Contains[[]int, int](c.UserIDs, int(gid)) || (c.currentUserCache != nil && c.currentUserCache.ID == int(gid)) {
// gid is a user
user, err := c.fetchUser(int(gid))
if err != nil {
return nil, nil, err
}
return c.fetchUserContent(user)
} else {
// gid is a group
group, err := c.fetchGroup(int(gid))
if err != nil {
return nil, nil, err
}
return c.fetchGroupContent(group)
}
}

View File

@ -4,64 +4,69 @@ import (
"fmt" "fmt"
"sync" "sync"
"github.com/badjware/gitlabfs/fs"
"github.com/xanzy/go-gitlab" "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 { type Group struct {
ID int ID int
Name string Name string
mux sync.Mutex mux sync.Mutex
content *GroupContent
// group content cache
groupCache map[string]fs.GroupSource
projectCache map[string]fs.RepositorySource
} }
func NewGroupFromGitlabGroup(group *gitlab.Group) Group { func (g *Group) GetGroupID() uint64 {
// https://godoc.org/github.com/xanzy/go-gitlab#Group return uint64(g.ID)
return Group{
ID: group.ID,
Name: group.Path,
}
} }
func (g *Group) InvalidateCache() { func (g *Group) InvalidateCache() {
g.mux.Lock() g.mux.Lock()
defer g.mux.Unlock() defer g.mux.Unlock()
g.content = nil g.groupCache = nil
g.projectCache = nil
} }
func (c *gitlabClient) FetchGroup(gid int) (*Group, error) { func (c *gitlabClient) fetchGroup(gid int) (*Group, error) {
// start by searching the cache
// TODO: cache invalidation?
group, found := c.groupCache[gid]
if found {
return group, nil
}
// If not in cache, fetch group infos from API
gitlabGroup, _, err := c.client.Groups.GetGroup(gid) gitlabGroup, _, err := c.client.Groups.GetGroup(gid)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch group with id %v: %v", gid, err) return nil, fmt.Errorf("failed to fetch group with id %v: %v", gid, err)
} }
group := NewGroupFromGitlabGroup(gitlabGroup) newGroup := Group{
return &group, nil ID: gitlabGroup.ID,
Name: gitlabGroup.Path,
groupCache: nil,
projectCache: nil,
}
// save in cache
c.groupCache[gid] = &newGroup
return &newGroup, nil
} }
func (c *gitlabClient) FetchGroupContent(group *Group) (*GroupContent, error) { func (c *gitlabClient) fetchGroupContent(group *Group) (map[string]fs.GroupSource, map[string]fs.RepositorySource, error) {
group.mux.Lock() group.mux.Lock()
defer group.mux.Unlock() defer group.mux.Unlock()
// Get cached data if available // Get cached data if available
if group.content != nil { // TODO: cache cache invalidation?
return group.content, nil if group.groupCache == nil || group.projectCache == nil {
} groupCache := make(map[string]fs.GroupSource)
projectCache := make(map[string]fs.RepositorySource)
content := &GroupContent{
Groups: map[string]*Group{},
Projects: map[string]*Project{},
}
// List subgroups in path // List subgroups in path
ListGroupsOpt := &gitlab.ListSubgroupsOptions{ ListGroupsOpt := &gitlab.ListSubgroupsOptions{
@ -74,11 +79,11 @@ func (c *gitlabClient) FetchGroupContent(group *Group) (*GroupContent, error) {
for { for {
gitlabGroups, response, err := c.client.Groups.ListSubgroups(group.ID, ListGroupsOpt) gitlabGroups, response, err := c.client.Groups.ListSubgroups(group.ID, ListGroupsOpt)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch groups in gitlab: %v", err) return nil, nil, fmt.Errorf("failed to fetch groups in gitlab: %v", err)
} }
for _, gitlabGroup := range gitlabGroups { for _, gitlabGroup := range gitlabGroups {
group := NewGroupFromGitlabGroup(gitlabGroup) group, _ := c.fetchGroup(gitlabGroup.ID)
content.Groups[group.Name] = &group groupCache[group.Name] = group
} }
if response.CurrentPage >= response.TotalPages { if response.CurrentPage >= response.TotalPages {
break break
@ -96,11 +101,11 @@ func (c *gitlabClient) FetchGroupContent(group *Group) (*GroupContent, error) {
for { for {
gitlabProjects, response, err := c.client.Groups.ListGroupProjects(group.ID, listProjectOpt) gitlabProjects, response, err := c.client.Groups.ListGroupProjects(group.ID, listProjectOpt)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch projects in gitlab: %v", err) return nil, nil, fmt.Errorf("failed to fetch projects in gitlab: %v", err)
} }
for _, gitlabProject := range gitlabProjects { for _, gitlabProject := range gitlabProjects {
project := c.newProjectFromGitlabProject(gitlabProject) project := c.newProjectFromGitlabProject(gitlabProject)
content.Projects[project.Name] = &project projectCache[project.Name] = &project
} }
if response.CurrentPage >= response.TotalPages { if response.CurrentPage >= response.TotalPages {
break break
@ -109,6 +114,8 @@ func (c *gitlabClient) FetchGroupContent(group *Group) (*GroupContent, error) {
listProjectOpt.Page = response.NextPage listProjectOpt.Page = response.NextPage
} }
group.content = content group.groupCache = groupCache
return content, nil group.projectCache = projectCache
}
return group.groupCache, group.projectCache, nil
} }

View File

@ -11,6 +11,18 @@ type Project struct {
DefaultBranch string DefaultBranch string
} }
func (p *Project) GetRepositoryID() uint64 {
return uint64(p.ID)
}
func (p *Project) GetCloneURL() string {
return p.CloneURL
}
func (p *Project) GetDefaultBranch() string {
return p.DefaultBranch
}
func (c *gitlabClient) newProjectFromGitlabProject(project *gitlab.Project) Project { func (c *gitlabClient) newProjectFromGitlabProject(project *gitlab.Project) Project {
// https://godoc.org/github.com/xanzy/go-gitlab#Project // https://godoc.org/github.com/xanzy/go-gitlab#Project
p := Project{ p := Project{

View File

@ -1,80 +1,85 @@
package gitlab package gitlab
import ( import (
"errors"
"fmt" "fmt"
"sync" "sync"
"github.com/badjware/gitlabfs/fs"
"github.com/xanzy/go-gitlab" "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 { type User struct {
ID int ID int
Name string Name string
mux sync.Mutex mux sync.Mutex
content *UserContent
// user content cache
projectCache map[string]fs.RepositorySource
} }
func NewUserFromGitlabUser(user *gitlab.User) User { func (u *User) GetGroupID() uint64 {
// https://godoc.org/github.com/xanzy/go-gitlab#User return uint64(u.ID)
return User{
ID: user.ID,
Name: user.Username,
}
} }
func (u *User) InvalidateCache() { func (u *User) InvalidateCache() {
u.mux.Lock() u.mux.Lock()
defer u.mux.Unlock() defer u.mux.Unlock()
u.content = nil u.projectCache = nil
} }
func (c *gitlabClient) FetchUser(uid int) (*User, error) { func (c *gitlabClient) fetchUser(uid int) (*User, error) {
// start by searching the cache
// TODO: cache invalidation?
user, found := c.userCache[uid]
if found {
return user, nil
}
// If not in cache, fetch group infos from API
gitlabUser, _, err := c.client.Users.GetUser(uid) gitlabUser, _, err := c.client.Users.GetUser(uid)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch user with id %v: %v", uid, err) return nil, fmt.Errorf("failed to fetch user with id %v: %v", uid, err)
} }
user := NewUserFromGitlabUser(gitlabUser) newUser := User{
return &user, nil ID: gitlabUser.ID,
Name: gitlabUser.Username,
projectCache: nil,
}
// save in cache
c.userCache[uid] = &newUser
return &newUser, nil
} }
func (c *gitlabClient) FetchCurrentUser() (*User, error) { func (c *gitlabClient) fetchCurrentUser() (*User, error) {
if c.IncludeCurrentUser { if c.currentUserCache == nil {
gitlabUser, _, err := c.client.Users.CurrentUser() gitlabUser, _, err := c.client.Users.CurrentUser()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch current user: %v", err) return nil, fmt.Errorf("failed to fetch current user: %v", err)
} }
user := NewUserFromGitlabUser(gitlabUser) newUser := User{
return &user, nil ID: gitlabUser.ID,
Name: gitlabUser.Username,
projectCache: nil,
} }
// no current user to fetch, return nil c.currentUserCache = &newUser
return nil, errors.New("current user fetch is disabled") }
return c.currentUserCache, nil
} }
func (c *gitlabClient) FetchUserContent(user *User) (*UserContent, error) { func (c *gitlabClient) fetchUserContent(user *User) (map[string]fs.GroupSource, map[string]fs.RepositorySource, error) {
user.mux.Lock() user.mux.Lock()
defer user.mux.Unlock() defer user.mux.Unlock()
// Get cached data if available // Get cached data if available
if user.content != nil { // TODO: cache cache invalidation?
return user.content, nil if user.projectCache == nil {
} projectCache := make(map[string]fs.RepositorySource)
content := &UserContent{
Projects: map[string]*Project{},
}
// Fetch the user repositories // Fetch the user repositories
listProjectOpt := &gitlab.ListProjectsOptions{ listProjectOpt := &gitlab.ListProjectsOptions{
@ -85,11 +90,11 @@ func (c *gitlabClient) FetchUserContent(user *User) (*UserContent, error) {
for { for {
gitlabProjects, response, err := c.client.Projects.ListUserProjects(user.ID, listProjectOpt) gitlabProjects, response, err := c.client.Projects.ListUserProjects(user.ID, listProjectOpt)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch projects in gitlab: %v", err) return nil, nil, fmt.Errorf("failed to fetch projects in gitlab: %v", err)
} }
for _, gitlabProject := range gitlabProjects { for _, gitlabProject := range gitlabProjects {
project := c.newProjectFromGitlabProject(gitlabProject) project := c.newProjectFromGitlabProject(gitlabProject)
content.Projects[project.Name] = &project projectCache[project.Name] = &project
} }
if response.CurrentPage >= response.TotalPages { if response.CurrentPage >= response.TotalPages {
break break
@ -98,6 +103,7 @@ func (c *gitlabClient) FetchUserContent(user *User) (*UserContent, error) {
listProjectOpt.Page = response.NextPage listProjectOpt.Page = response.NextPage
} }
user.content = content user.projectCache = projectCache
return content, nil }
return make(map[string]fs.GroupSource), user.projectCache, nil
} }

26
go.mod
View File

@ -1,18 +1,34 @@
module github.com/badjware/gitlabfs module github.com/badjware/gitlabfs
go 1.15 go 1.21
require (
github.com/hanwen/go-fuse/v2 v2.1.0
github.com/vmihailenco/taskq/v3 v3.2.9-0.20211122085105-720ffc56ac4d
github.com/xanzy/go-gitlab v0.47.0
gopkg.in/yaml.v2 v2.4.0
)
require ( require (
github.com/bsm/redislock v0.7.2 // indirect github.com/bsm/redislock v0.7.2 // indirect
github.com/capnm/sysinfo v0.0.0-20130621111458-5909a53897f3 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-redis/redis/v8 v8.11.4 // indirect
github.com/go-redis/redis_rate/v9 v9.1.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
github.com/hanwen/go-fuse/v2 v2.1.0
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.6.8 // indirect github.com/hashicorp/go-retryablehttp v0.6.8 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/klauspost/compress v1.14.4 // indirect github.com/klauspost/compress v1.14.4 // indirect
github.com/vmihailenco/taskq/v3 v3.2.9-0.20211122085105-720ffc56ac4d github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/xanzy/go-gitlab v0.47.0 github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 // indirect golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 // indirect
golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
gopkg.in/yaml.v2 v2.4.0 google.golang.org/protobuf v1.26.0 // indirect
) )

3
go.sum
View File

@ -131,7 +131,6 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/hanwen/go-fuse v1.0.0 h1:GxS9Zrn6c35/BnfiVsZVWmsG803xwE7eVRDvcf/BEVc=
github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok=
github.com/hanwen/go-fuse/v2 v2.1.0 h1:+32ffteETaLYClUj0a3aHjZ1hOPxxaNEHiZiujuDaek= github.com/hanwen/go-fuse/v2 v2.1.0 h1:+32ffteETaLYClUj0a3aHjZ1hOPxxaNEHiZiujuDaek=
github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc= github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc=
@ -151,11 +150,9 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/iron-io/iron_go3 v0.0.0-20190916120531-a4a7f74b73ac h1:w5wltlINIIqRTqQ64dASrCo0fM7k9nosPbKCZnkL0W0= github.com/iron-io/iron_go3 v0.0.0-20190916120531-a4a7f74b73ac h1:w5wltlINIIqRTqQ64dASrCo0fM7k9nosPbKCZnkL0W0=
github.com/iron-io/iron_go3 v0.0.0-20190916120531-a4a7f74b73ac/go.mod h1:gyMTRVO+ZkEy7wQDyD++okPsBN2q127EpuShhHMWG54= github.com/iron-io/iron_go3 v0.0.0-20190916120531-a4a7f74b73ac/go.mod h1:gyMTRVO+ZkEy7wQDyD++okPsBN2q127EpuShhHMWG54=
github.com/jeffh/go.bdd v0.0.0-20120717032931-88f798ee0c74 h1:gyfyP8SEIZHs1u2ivTdIbWRtfaKbg5K79d06vnqroJo=
github.com/jeffh/go.bdd v0.0.0-20120717032931-88f798ee0c74/go.mod h1:qNa9FlAfO0U/qNkzYBMH1JKYRMzC+sP9IcyV4U18l98= github.com/jeffh/go.bdd v0.0.0-20120717032931-88f798ee0c74/go.mod h1:qNa9FlAfO0U/qNkzYBMH1JKYRMzC+sP9IcyV4U18l98=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=

29
main.go
View File

@ -17,24 +17,16 @@ import (
type ( type (
Config struct { Config struct {
FS FSConfig `yaml:"fs,omitempty"` FS FSConfig `yaml:"fs,omitempty"`
Gitlab GitlabConfig `yaml:"gitlab,omitempty"` Gitlab gitlab.GitlabClientConfig `yaml:"gitlab,omitempty"`
Git GitConfig `yaml:"git,omitempty"` Git GitConfig `yaml:"git,omitempty"`
} }
FSConfig struct { FSConfig struct {
Mountpoint string `yaml:"mountpoint,omitempty"` Mountpoint string `yaml:"mountpoint,omitempty"`
MountOptions string `yaml:"mountoptions,omitempty"` MountOptions string `yaml:"mountoptions,omitempty"`
} }
GitlabConfig struct {
URL string `yaml:"url,omitempty"`
Token string `yaml:"token,omitempty"`
GroupIDs []int `yaml:"group_ids,omitempty"`
UserIDs []int `yaml:"user_ids,omitempty"`
IncludeCurrentUser bool `yaml:"include_current_user,omitempty"`
}
GitConfig struct { GitConfig struct {
CloneLocation string `yaml:"clone_location,omitempty"` CloneLocation string `yaml:"clone_location,omitempty"`
Remote string `yaml:"remote,omitempty"` Remote string `yaml:"remote,omitempty"`
PullMethod string `yaml:"pull_method,omitempty"`
OnClone string `yaml:"on_clone,omitempty"` OnClone string `yaml:"on_clone,omitempty"`
AutoPull bool `yaml:"auto_pull,omitempty"` AutoPull bool `yaml:"auto_pull,omitempty"`
Depth int `yaml:"depth,omitempty"` Depth int `yaml:"depth,omitempty"`
@ -56,17 +48,17 @@ func loadConfig(configPath string) (*Config, error) {
Mountpoint: "", Mountpoint: "",
MountOptions: "nodev,nosuid", MountOptions: "nodev,nosuid",
}, },
Gitlab: GitlabConfig{ Gitlab: gitlab.GitlabClientConfig{
URL: "https://gitlab.com", URL: "https://gitlab.com",
Token: "", Token: "",
GroupIDs: []int{9970}, GroupIDs: []int{9970},
UserIDs: []int{}, UserIDs: []int{},
IncludeCurrentUser: true, IncludeCurrentUser: true,
PullMethod: "http",
}, },
Git: GitConfig{ Git: GitConfig{
CloneLocation: defaultCloneLocation, CloneLocation: defaultCloneLocation,
Remote: "origin", Remote: "origin",
PullMethod: "http",
OnClone: "init", OnClone: "init",
AutoPull: false, AutoPull: false,
Depth: 0, Depth: 0,
@ -91,16 +83,13 @@ func loadConfig(configPath string) (*Config, error) {
return config, nil return config, nil
} }
func makeGitlabConfig(config *Config) (*gitlab.GitlabClientParam, error) { func makeGitlabConfig(config *Config) (*gitlab.GitlabClientConfig, error) {
// parse pull_method // parse pull_method
if config.Git.PullMethod != gitlab.PullMethodHTTP && config.Git.PullMethod != gitlab.PullMethodSSH { if config.Gitlab.PullMethod != gitlab.PullMethodHTTP && config.Gitlab.PullMethod != gitlab.PullMethodSSH {
return nil, fmt.Errorf("pull_method must be either \"%v\" or \"%v\"", gitlab.PullMethodHTTP, gitlab.PullMethodSSH) return nil, fmt.Errorf("pull_method must be either \"%v\" or \"%v\"", gitlab.PullMethodHTTP, gitlab.PullMethodSSH)
} }
return &gitlab.GitlabClientParam{ return &config.Gitlab, nil
PullMethod: config.Git.PullMethod,
IncludeCurrentUser: config.Gitlab.IncludeCurrentUser && config.Gitlab.Token != "",
}, nil
} }
func makeGitConfig(config *Config) (*git.GitClientParam, error) { func makeGitConfig(config *Config) (*git.GitClientParam, error) {
@ -181,18 +170,18 @@ func main() {
gitClient, _ := git.NewClient(*gitClientParam) gitClient, _ := git.NewClient(*gitClientParam)
// Create the gitlab client // Create the gitlab client
gitlabClientParam, err := makeGitlabConfig(config) GitlabClientConfig, err := makeGitlabConfig(config)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
gitlabClient, _ := gitlab.NewClient(config.Gitlab.URL, config.Gitlab.Token, *gitlabClientParam) gitlabClient, _ := gitlab.NewClient(config.Gitlab.URL, config.Gitlab.Token, *GitlabClientConfig)
// Start the filesystem // Start the filesystem
err = fs.Start( err = fs.Start(
mountpoint, mountpoint,
parsedMountoptions, parsedMountoptions,
&fs.FSParam{Git: gitClient, Gitlab: gitlabClient, RootGroupIds: config.Gitlab.GroupIDs, UserIds: config.Gitlab.UserIDs}, &fs.FSParam{GitImplementation: gitClient, GitPlatform: gitlabClient},
*debug, *debug,
) )
if err != nil { if err != nil {