Compare commits
38 Commits
d309d20dd8
...
271e63c960
Author | SHA1 | Date |
---|---|---|
Massaki Archambault | 271e63c960 | |
Massaki Archambault | 900375c7b3 | |
Massaki Archambault | b85b3c679d | |
Massaki Archambault | 8a6e5b122b | |
Massaki Archambault | 1ab51a10a1 | |
Massaki Archambault | 666186c699 | |
Massaki Archambault | 710dd50091 | |
Massaki Archambault | 34f3b904e9 | |
Massaki Archambault | 68cbf311b8 | |
Massaki Archambault | 23e78ec886 | |
Massaki Archambault | 3ea4cfcbf8 | |
Massaki Archambault | 9009c4b07f | |
Massaki Archambault | d37b3eb0eb | |
Massaki Archambault | 36f635b46e | |
Massaki Archambault | 2dc97f1672 | |
Massaki Archambault | 3e84321e85 | |
Massaki Archambault | c8f3de248f | |
Massaki Archambault | 367d770371 | |
Massaki Archambault | c14a9ce30c | |
Massaki Archambault | 7b06b68dbd | |
Massaki Archambault | baf013f834 | |
Massaki Archambault | 0ddc4e515e | |
Massaki Archambault | abf8507673 | |
Massaki Archambault | dca46e8c69 | |
Massaki Archambault | 7856de56b5 | |
Massaki Archambault | 471b0061b5 | |
Massaki Archambault | 65d3a00fa3 | |
Massaki Archambault | 96f250fc47 | |
Massaki Archambault | 7fccd4b91d | |
Massaki Archambault | 8293825c2b | |
Massaki Archambault | 8c53ccea6a | |
Massaki Archambault | 074cc9a349 | |
Massaki Archambault | e9670d59a1 | |
Massaki Archambault | 38362eebdd | |
Massaki Archambault | 45cef75960 | |
Massaki Archambault | 735a803cdb | |
Massaki Archambault | 0c647a692f | |
Massaki Archambault | 5ed64d523e |
|
@ -53,4 +53,5 @@ tags
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/go,vim,code
|
# End of https://www.toptal.com/developers/gitignore/api/go,vim,code
|
||||||
|
|
||||||
config.yaml
|
config.yaml
|
||||||
|
__debug_*
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Run",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "debug",
|
||||||
|
"program": "${workspaceRoot}",
|
||||||
|
"args": [
|
||||||
|
"-debug",
|
||||||
|
"-config",
|
||||||
|
"${workspaceRoot}/config.yaml"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
24
README.md
24
README.md
|
@ -52,7 +52,7 @@ Merge requests to add support to other forges are welcome.
|
||||||
|
|
||||||
Install [go](https://golang.org/) and run
|
Install [go](https://golang.org/) and run
|
||||||
``` sh
|
``` sh
|
||||||
go install github.com/badjware/gitforgefs
|
go install github.com/badjware/gitforgefs@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
The executable will be in `$GOPATH/bin/gitforgefs` or `~/go/bin/gitforgefs` by default. For convenience, add `~/go/bin` in your `$PATH` if not done already.
|
The executable will be in `$GOPATH/bin/gitforgefs` or `~/go/bin/gitforgefs` by default. For convenience, add `~/go/bin` in your `$PATH` if not done already.
|
||||||
|
@ -92,4 +92,24 @@ While the filesystem lives in memory, the git repositories that are cloned are s
|
||||||
|
|
||||||
Simply use `make` to create the executable. The executable will be in `bin/`.
|
Simply use `make` to create the executable. The executable will be in `bin/`.
|
||||||
|
|
||||||
See `make help` for all available targets.
|
See `make help` for all available targets.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### My application claims that a file or a folder doesn't exists.
|
||||||
|
|
||||||
|
Some applications doesn't resolve symlinks properly. Try setting the `fs.use_symlinks` configuration to `false`.
|
||||||
|
|
||||||
|
### `docker` fails to run with the message _error while creating mount source path_
|
||||||
|
|
||||||
|
This happens because `docker` is running as a different user than the one who created the mount. Follow these steps to allow docker access to the mount:
|
||||||
|
|
||||||
|
1. Open the file `/etc/fuse.conf` as root.
|
||||||
|
2. Add `user_allow_other` to the file, then close and save your modifications.
|
||||||
|
3. Open your `gitforgefs` configuration.
|
||||||
|
4. Add the `allow_other` to your mountoptions. The mount option are configured in `fs.mountoptions`.
|
||||||
|
``` yaml
|
||||||
|
fs:
|
||||||
|
mountoptions: allow_other,nodev,nosuid
|
||||||
|
```
|
||||||
|
5. Restart your mount.
|
||||||
|
|
|
@ -3,8 +3,15 @@ fs:
|
||||||
#mountpoint: /mnt
|
#mountpoint: /mnt
|
||||||
|
|
||||||
# Mount options to pass to `fusermount` as its `-o` argument. Can be overwritten via the command line.
|
# Mount options to pass to `fusermount` as its `-o` argument. Can be overwritten via the command line.
|
||||||
|
# Some applications need the `allow_other` option to function properly (eg: docker). If you need to use `allow_other`,
|
||||||
|
# you must also add `user_allow_other` in /etc/fuse.conf.
|
||||||
# See mount.fuse(8) for the full list of options.
|
# See mount.fuse(8) for the full list of options.
|
||||||
#mountoptions: nodev,nosuid
|
#mountoptions: nodev,nosuid
|
||||||
|
#mountoptions: allow_other,nodev,nosuid
|
||||||
|
|
||||||
|
# Use a symlink to point to the real location of the repository instead of doing a loopback
|
||||||
|
# Using symlinks is more performant and allow cloning to be asynchronous, but may cause compatibility issues with some applications
|
||||||
|
# use_symlinks: false
|
||||||
|
|
||||||
# The git forge to use as the backend.
|
# The git forge to use as the backend.
|
||||||
# Must be one of "gitlab", "github", or "gitea"
|
# Must be one of "gitlab", "github", or "gitea"
|
||||||
|
|
|
@ -32,6 +32,7 @@ type (
|
||||||
FSConfig struct {
|
FSConfig struct {
|
||||||
Mountpoint string `yaml:"mountpoint,omitempty"`
|
Mountpoint string `yaml:"mountpoint,omitempty"`
|
||||||
MountOptions string `yaml:"mountoptions,omitempty"`
|
MountOptions string `yaml:"mountoptions,omitempty"`
|
||||||
|
UseSymlinks bool `yaml:"use_symlinks,omitempty"`
|
||||||
Forge string `yaml:"forge,omitempty"`
|
Forge string `yaml:"forge,omitempty"`
|
||||||
}
|
}
|
||||||
GitlabClientConfig struct {
|
GitlabClientConfig struct {
|
||||||
|
@ -89,6 +90,7 @@ func LoadConfig(configPath string) (*Config, error) {
|
||||||
FS: FSConfig{
|
FS: FSConfig{
|
||||||
Mountpoint: "",
|
Mountpoint: "",
|
||||||
MountOptions: "nodev,nosuid",
|
MountOptions: "nodev,nosuid",
|
||||||
|
UseSymlinks: false,
|
||||||
Forge: "",
|
Forge: "",
|
||||||
},
|
},
|
||||||
Gitlab: GitlabClientConfig{
|
Gitlab: GitlabClientConfig{
|
||||||
|
|
|
@ -31,7 +31,7 @@ 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 newGroupNodeFromSource(source GroupSource, param *FSParam) (*groupNode, error) {
|
func newGroupNodeFromSource(ctx context.Context, source GroupSource, param *FSParam) (fs.InodeEmbedder, error) {
|
||||||
node := &groupNode{
|
node := &groupNode{
|
||||||
param: param,
|
param: param,
|
||||||
source: source,
|
source: source,
|
||||||
|
@ -85,7 +85,7 @@ func (n *groupNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut)
|
||||||
Ino: group.GetGroupID() + groupBaseInode,
|
Ino: group.GetGroupID() + groupBaseInode,
|
||||||
Mode: fuse.S_IFDIR,
|
Mode: fuse.S_IFDIR,
|
||||||
}
|
}
|
||||||
groupNode, _ := newGroupNodeFromSource(group, n.param)
|
groupNode, _ := newGroupNodeFromSource(ctx, group, n.param)
|
||||||
return n.NewInode(ctx, groupNode, attrs), 0
|
return n.NewInode(ctx, groupNode, attrs), 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,10 +93,17 @@ func (n *groupNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut)
|
||||||
repository, found := repositories[name]
|
repository, found := repositories[name]
|
||||||
if found {
|
if found {
|
||||||
attrs := fs.StableAttr{
|
attrs := fs.StableAttr{
|
||||||
Ino: repository.GetRepositoryID() + repositoryBaseInode,
|
Ino: repository.GetRepositoryID() + repositoryBaseInode,
|
||||||
Mode: fuse.S_IFLNK,
|
}
|
||||||
|
if n.param.UseSymlinks {
|
||||||
|
attrs.Mode = fuse.S_IFLNK
|
||||||
|
} else {
|
||||||
|
attrs.Mode = fuse.S_IFDIR
|
||||||
|
}
|
||||||
|
repositoryNode, err := newRepositoryNodeFromSource(ctx, repository, n.param)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
repositoryNode, _ := newRepositoryNodeFromSource(repository, n.param)
|
|
||||||
return n.NewInode(ctx, repositoryNode, attrs), 0
|
return n.NewInode(ctx, repositoryNode, attrs), 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,11 @@ package fstree
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/hanwen/go-fuse/v2/fs"
|
"github.com/hanwen/go-fuse/v2/fs"
|
||||||
)
|
)
|
||||||
|
@ -11,7 +15,7 @@ const (
|
||||||
repositoryBaseInode = 2_000_000_000
|
repositoryBaseInode = 2_000_000_000
|
||||||
)
|
)
|
||||||
|
|
||||||
type repositoryNode struct {
|
type repositorySymlinkNode struct {
|
||||||
fs.Inode
|
fs.Inode
|
||||||
param *FSParam
|
param *FSParam
|
||||||
|
|
||||||
|
@ -26,24 +30,53 @@ type RepositorySource interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we are implementing the NodeReaddirer interface
|
// Ensure we are implementing the NodeReaddirer interface
|
||||||
var _ = (fs.NodeReadlinker)((*repositoryNode)(nil))
|
var _ = (fs.NodeReadlinker)((*repositorySymlinkNode)(nil))
|
||||||
|
|
||||||
func newRepositoryNodeFromSource(source RepositorySource, param *FSParam) (*repositoryNode, error) {
|
func newRepositoryNodeFromSource(ctx context.Context, source RepositorySource, param *FSParam) (fs.InodeEmbedder, error) {
|
||||||
node := &repositoryNode{
|
if param.UseSymlinks {
|
||||||
param: param,
|
return &repositorySymlinkNode{
|
||||||
source: source,
|
param: param,
|
||||||
|
source: source,
|
||||||
|
}, nil
|
||||||
|
} else {
|
||||||
|
localRepositoryPath, err := param.GitClient.FetchLocalRepositoryPath(ctx, source)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch local repository path: %w", err)
|
||||||
|
}
|
||||||
|
// The path must exist to successfully create a loopback. We poll the filesystem until its created.
|
||||||
|
// This of course add latency, maybe we should think of a way of mitigating it in the future.
|
||||||
|
// We do not care in the case of a symlink. A symlink pointing on nothing is still a valid symlink.
|
||||||
|
for ctx.Err() == nil {
|
||||||
|
_, err := os.Stat(localRepositoryPath)
|
||||||
|
if err == nil {
|
||||||
|
return fs.NewLoopbackRoot(localRepositoryPath)
|
||||||
|
} else if errors.Is(err, os.ErrNotExist) {
|
||||||
|
// wait for the file to be created
|
||||||
|
// TODO: think of a more efficient way of archiving this
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
} else {
|
||||||
|
// error, filesystem
|
||||||
|
return nil, fmt.Errorf("error while waiting for the local repository to be created: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// error, context cancelled
|
||||||
|
return nil, fmt.Errorf("context cancelled while waiting for the local repository to be created: %w", ctx.Err())
|
||||||
}
|
}
|
||||||
// Passthrough the error if there is one, nothing to add here
|
|
||||||
// Errors on clone/pull are non-fatal
|
|
||||||
return node, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *repositoryNode) Readlink(ctx context.Context) ([]byte, syscall.Errno) {
|
func (n *repositorySymlinkNode) Readlink(ctx context.Context) ([]byte, syscall.Errno) {
|
||||||
// Create the local copy of the repo
|
// Create the local copy of the repo
|
||||||
// TODO: cleanup
|
// TODO: cleanup
|
||||||
localRepositoryPath, err := n.param.GitClient.FetchLocalRepositoryPath(n.source)
|
localRepositoryPath, err := n.param.GitClient.FetchLocalRepositoryPath(ctx, n.source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
n.param.logger.Error(err.Error())
|
n.param.logger.Error(err.Error())
|
||||||
}
|
}
|
||||||
return []byte(localRepositoryPath), 0
|
return []byte(localRepositoryPath), 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type repositoryLoopbackNode struct {
|
||||||
|
fs.LoopbackNode
|
||||||
|
param *FSParam
|
||||||
|
|
||||||
|
source RepositorySource
|
||||||
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ type staticNode interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type GitClient interface {
|
type GitClient interface {
|
||||||
FetchLocalRepositoryPath(source RepositorySource) (string, error)
|
FetchLocalRepositoryPath(ctx context.Context, source RepositorySource) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type GitForge interface {
|
type GitForge interface {
|
||||||
|
@ -28,6 +28,8 @@ type GitForge interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type FSParam struct {
|
type FSParam struct {
|
||||||
|
UseSymlinks bool
|
||||||
|
|
||||||
GitClient GitClient
|
GitClient GitClient
|
||||||
GitForge GitForge
|
GitForge GitForge
|
||||||
|
|
||||||
|
@ -75,7 +77,7 @@ func (n *rootNode) OnAdd(ctx context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for groupName, group := range rootGroups {
|
for groupName, group := range rootGroups {
|
||||||
groupNode, _ := newGroupNodeFromSource(group, n.param)
|
groupNode, _ := newGroupNodeFromSource(ctx, group, n.param)
|
||||||
persistentInode := n.NewPersistentInode(
|
persistentInode := n.NewPersistentInode(
|
||||||
ctx,
|
ctx,
|
||||||
groupNode,
|
groupNode,
|
||||||
|
|
|
@ -84,7 +84,7 @@ func NewClient(logger *slog.Logger, p config.GitClientConfig) (*gitClient, error
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *gitClient) FetchLocalRepositoryPath(source fstree.RepositorySource) (localRepoLoc string, err error) {
|
func (c *gitClient) FetchLocalRepositoryPath(ctx context.Context, source fstree.RepositorySource) (localRepoLoc string, err error) {
|
||||||
rid := source.GetRepositoryID()
|
rid := source.GetRepositoryID()
|
||||||
cloneUrl := source.GetCloneURL()
|
cloneUrl := source.GetCloneURL()
|
||||||
defaultBranch := source.GetDefaultBranch()
|
defaultBranch := source.GetDefaultBranch()
|
||||||
|
@ -98,12 +98,12 @@ func (c *gitClient) FetchLocalRepositoryPath(source fstree.RepositorySource) (lo
|
||||||
localRepoLoc = filepath.Join(c.CloneLocation, hostname, strconv.Itoa(int(rid)))
|
localRepoLoc = filepath.Join(c.CloneLocation, hostname, strconv.Itoa(int(rid)))
|
||||||
if _, err := os.Stat(localRepoLoc); os.IsNotExist(err) {
|
if _, err := os.Stat(localRepoLoc); os.IsNotExist(err) {
|
||||||
// Dispatch clone msg
|
// Dispatch clone msg
|
||||||
msg := c.cloneTask.WithArgs(context.Background(), cloneUrl, defaultBranch, localRepoLoc)
|
msg := c.cloneTask.WithArgs(ctx, cloneUrl, defaultBranch, localRepoLoc)
|
||||||
msg.OnceInPeriod(time.Second, rid)
|
msg.OnceInPeriod(time.Second, rid)
|
||||||
c.queue.Add(msg)
|
c.queue.Add(msg)
|
||||||
} else if c.AutoPull {
|
} else if c.AutoPull {
|
||||||
// Dispatch pull msg
|
// Dispatch pull msg
|
||||||
msg := c.pullTask.WithArgs(context.Background(), localRepoLoc, defaultBranch)
|
msg := c.pullTask.WithArgs(ctx, localRepoLoc, defaultBranch)
|
||||||
msg.OnceInPeriod(time.Second, rid)
|
msg.OnceInPeriod(time.Second, rid)
|
||||||
c.queue.Add(msg)
|
c.queue.Add(msg)
|
||||||
}
|
}
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -5,7 +5,7 @@ go 1.21
|
||||||
require (
|
require (
|
||||||
code.gitea.io/sdk/gitea v0.19.0
|
code.gitea.io/sdk/gitea v0.19.0
|
||||||
github.com/google/go-github/v63 v63.0.0
|
github.com/google/go-github/v63 v63.0.0
|
||||||
github.com/hanwen/go-fuse/v2 v2.5.1
|
github.com/hanwen/go-fuse/v2 v2.7.2
|
||||||
github.com/vmihailenco/taskq/v3 v3.2.9
|
github.com/vmihailenco/taskq/v3 v3.2.9
|
||||||
github.com/xanzy/go-gitlab v0.107.0
|
github.com/xanzy/go-gitlab v0.107.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -59,6 +59,8 @@ 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/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ=
|
github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ=
|
||||||
github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs=
|
github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs=
|
||||||
|
github.com/hanwen/go-fuse/v2 v2.7.2 h1:SbJP1sUP+n1UF8NXBA14BuojmTez+mDgOk0bC057HQw=
|
||||||
|
github.com/hanwen/go-fuse/v2 v2.7.2/go.mod h1:ugNaD/iv5JYyS1Rcvi57Wz7/vrLQJo10mmketmoef48=
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||||
|
|
6
main.go
6
main.go
|
@ -98,7 +98,11 @@ func main() {
|
||||||
logger,
|
logger,
|
||||||
mountpoint,
|
mountpoint,
|
||||||
parsedMountoptions,
|
parsedMountoptions,
|
||||||
&fstree.FSParam{GitClient: gitClient, GitForge: gitForgeClient},
|
&fstree.FSParam{
|
||||||
|
UseSymlinks: loadedConfig.FS.UseSymlinks,
|
||||||
|
GitClient: gitClient,
|
||||||
|
GitForge: gitForgeClient,
|
||||||
|
},
|
||||||
*debug,
|
*debug,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in New Issue