├── ghfs.go ├── ghfs_test.go ├── LICENSE ├── README.md └── cmd └── ghfs └── main.go /ghfs.go: -------------------------------------------------------------------------------- 1 | package ghfs 2 | -------------------------------------------------------------------------------- /ghfs_test.go: -------------------------------------------------------------------------------- 1 | package ghfs 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ben Johnson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ghfs [![godoc](https://godoc.org/github.com/benbjohnson/ghfs?status.png)](https://godoc.org/github.com/benbjohnson/ghfs) ![Version](http://img.shields.io/badge/status-alpha-red.png) 2 | ==== 3 | 4 | The GitHub Filesystem (GHFS) is a user space filesystem that overlays the 5 | GitHub API. It allows you to access repositories and files using standard 6 | Unix commands such as `ls` and `cat`. 7 | 8 | 9 | ## Install 10 | 11 | To use ghfs, you'll need to install [Go][go]. If you're running OS X then you'll 12 | also need to install [MacFUSE][macfuse]. Then you can install ghfs by running: 13 | 14 | ```sh 15 | $ go get github.com/benbjohnson/ghfs/... 16 | ``` 17 | 18 | This will install `ghfs` into your `$GOBIN` directory. Next you'll need to 19 | create a directory and use `ghfs` to mount GitHub: 20 | 21 | ```sh 22 | $ mkdir ~/github 23 | $ ghfs ~/github & 24 | ``` 25 | 26 | Now you can read data from the GitHub API via the `~/github` directory. 27 | 28 | [go]: https://golang.org 29 | [macfuse]: https://osxfuse.github.io 30 | 31 | 32 | ## Usage 33 | 34 | GHFS uses GitHub URL conventions for pathing. For example, to go to a user 35 | you can `cd` using their username: 36 | 37 | ```sh 38 | $ cd ~/github/boltdb 39 | ``` 40 | 41 | To go to a repository, you can use the username and repository name: 42 | 43 | ```sh 44 | $ cd ~/github/boltdb/bolt 45 | ``` 46 | 47 | Once you're in a repository, you can list files using `ls` and you can print 48 | out file contents using the `cat` tool. 49 | 50 | ```sh 51 | bolt $ cat LICENSE 52 | The MIT License (MIT) 53 | 54 | Copyright (c) 2013 Ben Johnson 55 | 56 | Permission is hereby granted, free of charge, to any person obtaining a copy of 57 | this software and associated documentation files (the "Software"), to deal in 58 | ... 59 | ``` 60 | 61 | 62 | -------------------------------------------------------------------------------- /cmd/ghfs/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "flag" 6 | "io" 7 | "log" 8 | "net/http" 9 | "os" 10 | "strings" 11 | 12 | "bazil.org/fuse" 13 | "bazil.org/fuse/fs" 14 | "code.google.com/p/goauth2/oauth" 15 | "github.com/google/go-github/github" 16 | "golang.org/x/net/context" 17 | ) 18 | 19 | func main() { 20 | log.SetFlags(0) 21 | 22 | // Parse arguments and require that we have the path. 23 | token := flag.String("token", "", "personal access token") 24 | flag.Parse() 25 | if flag.NArg() != 1 { 26 | log.Fatal("path required") 27 | } 28 | log.Printf("mounting to: %s", flag.Arg(0)) 29 | 30 | // Create FUSE connection. 31 | conn, err := fuse.Mount(flag.Arg(0)) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | // Create OAuth transport. 37 | var c *http.Client 38 | if *token != "" { 39 | c = (&oauth.Transport{Token: &oauth.Token{AccessToken: *token}}).Client() 40 | } 41 | 42 | // Create filesystem. 43 | filesys := &FS{Client: github.NewClient(c)} 44 | if err := fs.Serve(conn, filesys); err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | // Wait until the mount is unmounted or there is an error. 49 | <-conn.Ready 50 | if err := conn.MountError; err != nil { 51 | log.Fatal(err) 52 | } 53 | } 54 | 55 | // FS represents the 56 | type FS struct { 57 | Client *github.Client 58 | } 59 | 60 | // Root returns the root filesystem node. 61 | func (f *FS) Root() (fs.Node, error) { 62 | return &Root{FS: f}, nil 63 | } 64 | 65 | type Root struct { 66 | FS *FS 67 | } 68 | 69 | func (r *Root) Attr() fuse.Attr { 70 | return fuse.Attr{Mode: os.ModeDir | 0755} 71 | } 72 | 73 | func (r *Root) Lookup(ctx context.Context, req *fuse.LookupRequest, resp *fuse.LookupResponse) (fs.Node, error) { 74 | if strings.HasPrefix(req.Name, ".") { 75 | return nil, fuse.ENOENT 76 | } 77 | 78 | u, _, err := r.FS.Client.Users.Get(req.Name) 79 | if err != nil { 80 | return nil, fuse.ENOENT 81 | } 82 | return &User{FS: r.FS, User: u}, nil 83 | } 84 | 85 | type User struct { 86 | *github.User 87 | FS *FS 88 | } 89 | 90 | func (u *User) Attr() fuse.Attr { 91 | return fuse.Attr{Mode: os.ModeDir | 0755} 92 | } 93 | 94 | func (u *User) Lookup(ctx context.Context, req *fuse.LookupRequest, resp *fuse.LookupResponse) (fs.Node, error) { 95 | if strings.HasPrefix(req.Name, ".") { 96 | return nil, fuse.ENOENT 97 | } 98 | 99 | r, _, err := u.FS.Client.Repositories.Get(*u.Login, req.Name) 100 | if err != nil { 101 | return nil, fuse.ENOENT 102 | } 103 | return &Repository{FS: u.FS, Repository: r}, nil 104 | } 105 | 106 | type Repository struct { 107 | *github.Repository 108 | FS *FS 109 | } 110 | 111 | var _ = fs.HandleReadDirAller(&Repository{}) 112 | 113 | func (r *Repository) Attr() fuse.Attr { 114 | return fuse.Attr{Mode: os.ModeDir | 0755} 115 | } 116 | 117 | func (r *Repository) Lookup(ctx context.Context, req *fuse.LookupRequest, resp *fuse.LookupResponse) (fs.Node, error) { 118 | if strings.HasPrefix(req.Name, ".") { 119 | return nil, fuse.ENOENT 120 | } 121 | 122 | fileContent, directoryContent, _, err := r.FS.Client.Repositories.GetContents(*r.Owner.Login, *r.Name, req.Name, nil) 123 | if err != nil { 124 | return nil, fuse.ENOENT 125 | } 126 | if fileContent != nil { 127 | return &File{FS: r.FS, Content: fileContent}, nil 128 | } 129 | return &Dir{FS: r.FS, Contents: directoryContent}, nil 130 | } 131 | 132 | func (r *Repository) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { 133 | _, directoryContent, _, err := r.FS.Client.Repositories.GetContents(*r.Owner.Login, *r.Name, "", nil) 134 | if err != nil { 135 | return nil, fuse.ENOENT 136 | } 137 | 138 | var entries []fuse.Dirent 139 | for _, f := range directoryContent { 140 | entries = append(entries, fuse.Dirent{Name: *f.Name}) 141 | } 142 | return entries, nil 143 | } 144 | 145 | type File struct { 146 | Content *github.RepositoryContent 147 | FS *FS 148 | } 149 | 150 | func (f *File) Attr() fuse.Attr { 151 | return fuse.Attr{ 152 | Size: uint64(*f.Content.Size), 153 | Mode: 0755, 154 | } 155 | } 156 | 157 | var _ = fs.NodeOpener(&File{}) 158 | 159 | func (f *File) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.OpenResponse) (fs.Handle, error) { 160 | resp.Flags |= fuse.OpenNonSeekable 161 | return &FileHandle{r: base64.NewDecoder(base64.StdEncoding, strings.NewReader(*f.Content.Content))}, nil 162 | } 163 | 164 | type FileHandle struct { 165 | r io.Reader 166 | } 167 | 168 | var _ = fs.HandleReader(&FileHandle{}) 169 | 170 | func (fh *FileHandle) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error { 171 | buf := make([]byte, req.Size) 172 | n, err := fh.r.Read(buf) 173 | resp.Data = buf[:n] 174 | return err 175 | } 176 | 177 | type Dir struct { 178 | Contents []*github.RepositoryContent 179 | FS *FS 180 | } 181 | 182 | func (d *Dir) Attr() fuse.Attr { 183 | return fuse.Attr{ 184 | Mode: os.ModeDir | 0755, 185 | } 186 | } 187 | --------------------------------------------------------------------------------