├── test ├── baz ├── baz3 ├── foo │ └── bar ├── foobar └── baz2 ├── file ├── .gitignore ├── demo └── main.go ├── LICENSE.txt ├── dir.go ├── file.go └── githubfs.go /test/baz: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/baz3: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/foo/bar: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/foobar: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /file: -------------------------------------------------------------------------------- 1 | Hello world -------------------------------------------------------------------------------- /test/baz2: -------------------------------------------------------------------------------- 1 | Hello world??Hello world.... 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | /local -------------------------------------------------------------------------------- /demo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | 8 | "github.com/google/go-github/github" 9 | "github.com/progrium/go-githubfs" 10 | "golang.org/x/oauth2" 11 | ) 12 | 13 | func main() { 14 | ctx := context.Background() 15 | ts := oauth2.StaticTokenSource( 16 | &oauth2.Token{AccessToken: os.Getenv("GITHUB_ACCESS_TOKEN")}, 17 | ) 18 | tc := oauth2.NewClient(ctx, ts) 19 | 20 | client := github.NewClient(tc) 21 | 22 | fs, err := githubfs.NewGitHubFs(client, "progrium", "go-githubfs", "master") 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | f, err := fs.OpenFile("test/baz2", os.O_APPEND, 0644) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | f.Write([]byte("Hello world....\n")) 32 | 33 | err = f.Close() 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | //fmt.Printf("%# v", pretty.Formatter(fs)) 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2017, Jeff Lindsay 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /dir.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2015 Steve Francia . 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package githubfs 15 | 16 | import "sort" 17 | 18 | type Dir interface { 19 | Len() int 20 | Names() []string 21 | Files() []*FileData 22 | Add(*FileData) 23 | Remove(*FileData) 24 | } 25 | 26 | func RemoveFromMemDir(dir *FileData, f *FileData) { 27 | dir.memDir.Remove(f) 28 | } 29 | 30 | func AddToMemDir(dir *FileData, f *FileData) { 31 | dir.memDir.Add(f) 32 | } 33 | 34 | type DirMap map[string]*FileData 35 | 36 | func (m DirMap) Len() int { return len(m) } 37 | func (m DirMap) Add(f *FileData) { m[f.name] = f } 38 | func (m DirMap) Remove(f *FileData) { delete(m, f.name) } 39 | func (m DirMap) Files() (files []*FileData) { 40 | for _, f := range m { 41 | files = append(files, f) 42 | } 43 | sort.Sort(filesSorter(files)) 44 | return files 45 | } 46 | 47 | // implement sort.Interface for []*FileData 48 | type filesSorter []*FileData 49 | 50 | func (s filesSorter) Len() int { return len(s) } 51 | func (s filesSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 52 | func (s filesSorter) Less(i, j int) bool { return s[i].name < s[j].name } 53 | 54 | func (m DirMap) Names() (names []string) { 55 | for x := range m { 56 | names = append(names, x) 57 | } 58 | return names 59 | } 60 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2015 Steve Francia . 2 | // Copyright 2013 tsuru authors. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package githubfs 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "encoding/base64" 21 | "errors" 22 | "io" 23 | "os" 24 | "path/filepath" 25 | "strings" 26 | "sync" 27 | "sync/atomic" 28 | "time" 29 | 30 | "github.com/google/go-github/github" 31 | ) 32 | 33 | const FilePathSeparator = string(filepath.Separator) 34 | 35 | type File struct { 36 | // atomic requires 64-bit alignment for struct field access 37 | at int64 38 | readDirCount int64 39 | closed bool 40 | readOnly bool 41 | fileData *FileData 42 | 43 | fs *githubFs 44 | entry github.TreeEntry 45 | } 46 | 47 | func NewFileHandle(data *FileData, fs *githubFs, entry github.TreeEntry) *File { 48 | return &File{fileData: data, fs: fs, entry: entry} 49 | } 50 | 51 | func NewReadOnlyFileHandle(data *FileData) *File { 52 | return &File{fileData: data, readOnly: true} 53 | } 54 | 55 | func (f File) Data() *FileData { 56 | return f.fileData 57 | } 58 | 59 | type FileData struct { 60 | sync.Mutex 61 | name string 62 | data []byte 63 | memDir Dir 64 | dir bool 65 | mode os.FileMode 66 | modtime time.Time 67 | } 68 | 69 | func (d *FileData) Name() string { 70 | d.Lock() 71 | defer d.Unlock() 72 | return d.name 73 | } 74 | 75 | func CreateFile(name string) *FileData { 76 | return &FileData{name: name, mode: os.ModeTemporary, modtime: time.Now()} 77 | } 78 | 79 | func CreateDir(name string) *FileData { 80 | return &FileData{name: name, memDir: &DirMap{}, dir: true} 81 | } 82 | 83 | func ChangeFileName(f *FileData, newname string) { 84 | f.Lock() 85 | f.name = newname 86 | f.Unlock() 87 | } 88 | 89 | func SetMode(f *FileData, mode os.FileMode) { 90 | f.Lock() 91 | f.mode = mode 92 | f.Unlock() 93 | } 94 | 95 | func SetModTime(f *FileData, mtime time.Time) { 96 | f.Lock() 97 | setModTime(f, mtime) 98 | f.Unlock() 99 | } 100 | 101 | func setModTime(f *FileData, mtime time.Time) { 102 | f.modtime = mtime 103 | } 104 | 105 | func GetFileInfo(f *FileData) *FileInfo { 106 | return &FileInfo{f} 107 | } 108 | 109 | func (f *File) Open() error { 110 | atomic.StoreInt64(&f.at, 0) 111 | atomic.StoreInt64(&f.readDirCount, 0) 112 | f.fileData.Lock() 113 | f.closed = false 114 | f.fileData.Unlock() 115 | return nil 116 | } 117 | 118 | func (f *File) Close() error { 119 | f.fileData.Lock() 120 | f.closed = true 121 | if !f.readOnly { 122 | setModTime(f.fileData, time.Now()) 123 | } 124 | f.fileData.Unlock() 125 | return f.Sync() // TODO: is this necessary? 126 | } 127 | 128 | func (f *File) Name() string { 129 | return f.fileData.Name() 130 | } 131 | 132 | func (f *File) Stat() (os.FileInfo, error) { 133 | return &FileInfo{f.fileData}, nil 134 | } 135 | 136 | func (f *File) Sync() error { 137 | if f.entry.GetType() == "tree" { 138 | return nil 139 | } 140 | f.fileData.Lock() 141 | blob, _, err := f.fs.client.Git.CreateBlob(context.TODO(), f.fs.user, f.fs.repo, &github.Blob{ 142 | Content: String(base64.StdEncoding.EncodeToString(f.fileData.data)), 143 | Encoding: String("base64"), 144 | }) 145 | f.fileData.Unlock() 146 | if err != nil { 147 | return err 148 | } 149 | f.fs.mu.Lock() 150 | defer f.fs.mu.Unlock() 151 | for i, e := range f.fs.tree.Entries { 152 | if e.GetPath() == f.entry.GetPath() { 153 | f.fs.tree.Entries[i].SHA = blob.SHA 154 | f.entry.SHA = blob.SHA 155 | } 156 | } 157 | if strings.Contains(f.entry.GetPath(), FilePathSeparator) { 158 | if err := f.fs.createTreesFromEntries(filepath.Dir(f.entry.GetPath()), true); err != nil { 159 | return err 160 | } 161 | } 162 | return f.fs.commit() 163 | } 164 | 165 | func (f *File) Readdir(count int) (res []os.FileInfo, err error) { 166 | var outLength int64 167 | 168 | f.fileData.Lock() 169 | files := f.fileData.memDir.Files()[f.readDirCount:] 170 | if count > 0 { 171 | if len(files) < count { 172 | outLength = int64(len(files)) 173 | } else { 174 | outLength = int64(count) 175 | } 176 | if len(files) == 0 { 177 | err = io.EOF 178 | } 179 | } else { 180 | outLength = int64(len(files)) 181 | } 182 | f.readDirCount += outLength 183 | f.fileData.Unlock() 184 | 185 | res = make([]os.FileInfo, outLength) 186 | for i := range res { 187 | res[i] = &FileInfo{files[i]} 188 | } 189 | 190 | return res, err 191 | } 192 | 193 | func (f *File) Readdirnames(n int) (names []string, err error) { 194 | fi, err := f.Readdir(n) 195 | names = make([]string, len(fi)) 196 | for i, f := range fi { 197 | _, names[i] = filepath.Split(f.Name()) 198 | } 199 | return names, err 200 | } 201 | 202 | func (f *File) Read(b []byte) (n int, err error) { 203 | f.fileData.Lock() 204 | defer f.fileData.Unlock() 205 | if f.closed == true { 206 | return 0, ErrFileClosed 207 | } 208 | if len(b) > 0 && int(f.at) == len(f.fileData.data) { 209 | return 0, io.EOF 210 | } 211 | if len(f.fileData.data)-int(f.at) >= len(b) { 212 | n = len(b) 213 | } else { 214 | n = len(f.fileData.data) - int(f.at) 215 | } 216 | copy(b, f.fileData.data[f.at:f.at+int64(n)]) 217 | atomic.AddInt64(&f.at, int64(n)) 218 | return 219 | } 220 | 221 | func (f *File) ReadAt(b []byte, off int64) (n int, err error) { 222 | atomic.StoreInt64(&f.at, off) 223 | return f.Read(b) 224 | } 225 | 226 | func (f *File) Truncate(size int64) error { 227 | if f.closed == true { 228 | return ErrFileClosed 229 | } 230 | if f.readOnly { 231 | return &os.PathError{Op: "truncate", Path: f.fileData.name, Err: errors.New("file handle is read only")} 232 | } 233 | if size < 0 { 234 | return ErrOutOfRange 235 | } 236 | if size > int64(len(f.fileData.data)) { 237 | diff := size - int64(len(f.fileData.data)) 238 | f.fileData.data = append(f.fileData.data, bytes.Repeat([]byte{00}, int(diff))...) 239 | } else { 240 | f.fileData.data = f.fileData.data[0:size] 241 | } 242 | setModTime(f.fileData, time.Now()) 243 | return nil 244 | } 245 | 246 | func (f *File) Seek(offset int64, whence int) (int64, error) { 247 | if f.closed == true { 248 | return 0, ErrFileClosed 249 | } 250 | switch whence { 251 | case 0: 252 | atomic.StoreInt64(&f.at, offset) 253 | case 1: 254 | atomic.AddInt64(&f.at, int64(offset)) 255 | case 2: 256 | atomic.StoreInt64(&f.at, int64(len(f.fileData.data))+offset) 257 | } 258 | return f.at, nil 259 | } 260 | 261 | func (f *File) Write(b []byte) (n int, err error) { 262 | if f.readOnly { 263 | return 0, &os.PathError{Op: "write", Path: f.fileData.name, Err: errors.New("file handle is read only")} 264 | } 265 | n = len(b) 266 | cur := atomic.LoadInt64(&f.at) 267 | f.fileData.Lock() 268 | defer f.fileData.Unlock() 269 | diff := cur - int64(len(f.fileData.data)) 270 | var tail []byte 271 | if n+int(cur) < len(f.fileData.data) { 272 | tail = f.fileData.data[n+int(cur):] 273 | } 274 | if diff > 0 { 275 | f.fileData.data = append(bytes.Repeat([]byte{00}, int(diff)), b...) 276 | f.fileData.data = append(f.fileData.data, tail...) 277 | } else { 278 | f.fileData.data = append(f.fileData.data[:cur], b...) 279 | f.fileData.data = append(f.fileData.data, tail...) 280 | } 281 | setModTime(f.fileData, time.Now()) 282 | 283 | atomic.StoreInt64(&f.at, int64(len(f.fileData.data))) 284 | return 285 | } 286 | 287 | func (f *File) WriteAt(b []byte, off int64) (n int, err error) { 288 | atomic.StoreInt64(&f.at, off) 289 | return f.Write(b) 290 | } 291 | 292 | func (f *File) WriteString(s string) (ret int, err error) { 293 | return f.Write([]byte(s)) 294 | } 295 | 296 | func (f *File) Info() *FileInfo { 297 | return &FileInfo{f.fileData} 298 | } 299 | 300 | type FileInfo struct { 301 | *FileData 302 | } 303 | 304 | // Implements os.FileInfo 305 | func (s *FileInfo) Name() string { 306 | s.Lock() 307 | _, name := filepath.Split(s.name) 308 | s.Unlock() 309 | return name 310 | } 311 | func (s *FileInfo) Mode() os.FileMode { 312 | s.Lock() 313 | defer s.Unlock() 314 | return s.mode 315 | } 316 | func (s *FileInfo) ModTime() time.Time { 317 | s.Lock() 318 | defer s.Unlock() 319 | return s.modtime 320 | } 321 | func (s *FileInfo) IsDir() bool { 322 | s.Lock() 323 | defer s.Unlock() 324 | return s.dir 325 | } 326 | func (s *FileInfo) Sys() interface{} { return nil } 327 | func (s *FileInfo) Size() int64 { 328 | if s.IsDir() { 329 | return int64(42) 330 | } 331 | s.Lock() 332 | defer s.Unlock() 333 | return int64(len(s.data)) 334 | } 335 | 336 | var ( 337 | ErrFileClosed = errors.New("File is closed") 338 | ErrOutOfRange = errors.New("Out of range") 339 | ErrTooLarge = errors.New("Too large") 340 | ErrFileNotFound = os.ErrNotExist 341 | ErrFileExists = os.ErrExist 342 | ErrDestinationExists = os.ErrExist 343 | ) 344 | -------------------------------------------------------------------------------- /githubfs.go: -------------------------------------------------------------------------------- 1 | package githubfs 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "path" 10 | "path/filepath" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/google/go-github/github" 16 | "github.com/spf13/afero" 17 | ) 18 | 19 | const CommitMessage = "automatic commit from githubfs 🎆" 20 | 21 | func String(s string) *string { 22 | return &s 23 | } 24 | 25 | type githubFs struct { 26 | client *github.Client 27 | user string 28 | repo string 29 | branch *github.Branch 30 | tree *github.Tree 31 | mu sync.Mutex 32 | } 33 | 34 | func NewGitHubFs(client *github.Client, user string, repo string, branch string) (afero.Fs, error) { 35 | fs := &githubFs{ 36 | client: client, 37 | user: user, 38 | repo: repo, 39 | } 40 | ctx := context.Background() 41 | var err error 42 | fs.branch, _, err = client.Repositories.GetBranch(ctx, user, repo, branch) 43 | if err != nil { 44 | return nil, err 45 | } 46 | err = fs.updateTree(fs.branch.Commit.Commit.Tree.GetSHA()) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return fs, nil 51 | } 52 | 53 | func (fs *githubFs) updateTree(sha string) (err error) { 54 | fs.tree, _, err = fs.client.Git.GetTree(context.TODO(), fs.user, fs.repo, sha, true) 55 | return 56 | } 57 | 58 | // Create creates a file in the filesystem, returning the file and an 59 | // error, if any happens. 60 | func (fs *githubFs) Create(name string) (afero.File, error) { 61 | fs.mu.Lock() 62 | defer fs.mu.Unlock() 63 | normalName := strings.TrimPrefix(name, "/") 64 | if normalName == "" { 65 | return nil, os.ErrInvalid 66 | } 67 | e := fs.findEntry(normalName) 68 | if e != nil { 69 | return nil, afero.ErrFileExists 70 | } 71 | var parent *github.TreeEntry 72 | if strings.Contains(normalName, FilePathSeparator) { 73 | parent = fs.findEntry(filepath.Dir(normalName)) 74 | if parent == nil { 75 | return nil, os.ErrNotExist 76 | } 77 | } 78 | blob, _, err := fs.client.Git.CreateBlob(context.TODO(), fs.user, fs.repo, &github.Blob{ 79 | Content: String(""), 80 | }) 81 | if err != nil { 82 | return nil, err 83 | } 84 | entry := github.TreeEntry{ 85 | Type: String("blob"), 86 | Mode: String("100644"), 87 | Path: String(normalName), 88 | SHA: blob.SHA, 89 | } 90 | fs.tree.Entries = append(fs.tree.Entries, entry) 91 | if parent != nil { 92 | err = fs.createTreesFromEntries(parent.GetPath(), false) 93 | if err != nil { 94 | return nil, err 95 | } 96 | } 97 | err = fs.commit() 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | // TODO: add necessary references 103 | fileData := CreateFile(name) 104 | file := NewFileHandle(fileData, fs, entry) 105 | 106 | return file, nil 107 | } 108 | 109 | func (fs *githubFs) createTreesFromEntries(path string, force bool) error { 110 | entry := fs.findEntry(path) 111 | if entry == nil { 112 | return fmt.Errorf("entry not found for path '%s'", path) 113 | } 114 | if entry.SHA == nil || force { 115 | var children []github.TreeEntry 116 | for _, e := range fs.tree.Entries { 117 | if strings.HasPrefix(e.GetPath(), path+"/") { 118 | relativeName := strings.TrimPrefix(e.GetPath(), path+"/") 119 | if !strings.Contains(relativeName, FilePathSeparator) { 120 | relativeEntry := e 121 | relativeEntry.Path = String(relativeName) 122 | children = append(children, relativeEntry) 123 | } 124 | } 125 | } 126 | tree, _, err := fs.client.Git.CreateTree(context.TODO(), fs.user, fs.repo, "", children) 127 | if err != nil { 128 | return err 129 | } 130 | for i, e := range fs.tree.Entries { 131 | if e.GetPath() == entry.GetPath() { 132 | fs.tree.Entries[i].SHA = tree.SHA 133 | } 134 | } 135 | } 136 | parentDir := filepath.Dir(path) 137 | if parentDir == "." || parentDir == "" { 138 | return nil 139 | } 140 | return fs.createTreesFromEntries(parentDir, force) 141 | } 142 | 143 | // Mkdir creates a directory in the filesystem, return an error if any 144 | // happens. 145 | func (fs *githubFs) Mkdir(name string, perm os.FileMode) error { 146 | fs.mu.Lock() 147 | defer fs.mu.Unlock() 148 | normalName := strings.TrimPrefix(name, "/") 149 | if strings.Contains(normalName, FilePathSeparator) { 150 | if p := fs.findEntry(filepath.Dir(normalName)); p == nil { 151 | return afero.ErrFileNotFound // parent path does not exist 152 | } 153 | } 154 | fs.tree.Entries = append(fs.tree.Entries, github.TreeEntry{ 155 | Type: String("tree"), 156 | Mode: String("040000"), 157 | Path: String(normalName), 158 | }) 159 | return nil 160 | } 161 | 162 | // MkdirAll creates a directory path and all parents that does not exist 163 | // yet. 164 | func (fs *githubFs) MkdirAll(path string, perm os.FileMode) error { 165 | normalName := strings.TrimPrefix(path, "/") 166 | parentNames := strings.Split(filepath.Dir(normalName), FilePathSeparator) 167 | if len(parentNames) == 0 { 168 | return fs.Mkdir(path, perm) 169 | } 170 | for i, _ := range parentNames { 171 | fs.mu.Lock() 172 | parentPath := strings.Join(parentNames[0:i+1], FilePathSeparator) 173 | parent := fs.findEntry(parentPath) 174 | fs.mu.Unlock() 175 | if parent == nil { 176 | err := fs.Mkdir(parentPath, perm) 177 | if err != nil { 178 | return err 179 | } 180 | } 181 | } 182 | return fs.Mkdir(path, perm) 183 | } 184 | 185 | func (fs *githubFs) findEntry(name string) *github.TreeEntry { 186 | normalName := strings.TrimPrefix(name, "/") 187 | for _, e := range fs.tree.Entries { 188 | if e.GetPath() == normalName { 189 | return &e 190 | } 191 | } 192 | return nil 193 | } 194 | 195 | func (fs *githubFs) open(name string) (afero.File, *FileData, error) { 196 | normalName := strings.TrimPrefix(name, "/") 197 | entry := fs.findEntry(name) 198 | if entry == nil { 199 | return nil, nil, afero.ErrFileNotFound 200 | } 201 | if entry.GetType() == "blob" { 202 | // if file 203 | fd := CreateFile(name) 204 | SetMode(fd, os.FileMode(int(0644))) 205 | blob, _, err := fs.client.Git.GetBlob(context.TODO(), fs.user, fs.repo, entry.GetSHA()) 206 | if err != nil { 207 | return nil, nil, err 208 | } 209 | fd.data, _ = base64.StdEncoding.DecodeString(blob.GetContent()) 210 | return NewFileHandle(fd, fs, *entry), fd, nil 211 | } 212 | // else if tree/dir 213 | dir := CreateDir(name) 214 | if normalName == "" { 215 | normalName = "." 216 | } 217 | for _, e := range fs.tree.Entries { 218 | if path.Dir(e.GetPath()) != normalName { 219 | continue 220 | } 221 | normalName := strings.TrimPrefix(e.GetPath(), path.Dir(e.GetPath())+"/") 222 | switch e.GetType() { 223 | case "blob": 224 | f := CreateFile(normalName) 225 | SetMode(f, os.FileMode(int(0644))) 226 | AddToMemDir(dir, f) 227 | case "tree": 228 | d := CreateDir(normalName) 229 | SetMode(d, os.FileMode(int(040000))) 230 | AddToMemDir(dir, d) 231 | default: 232 | continue 233 | } 234 | } 235 | return NewFileHandle(dir, fs, github.TreeEntry{Type: String("tree")}), dir, nil 236 | } 237 | 238 | // Open opens a file, returning it or an error, if any happens. 239 | func (fs *githubFs) Open(name string) (afero.File, error) { 240 | fs.mu.Lock() 241 | defer fs.mu.Unlock() 242 | f, _, err := fs.open(name) 243 | return f, err 244 | } 245 | 246 | // OpenFile opens a file using the given flags and the given mode. 247 | func (fs *githubFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) { 248 | fs.mu.Lock() 249 | _, fd, err := fs.open(name) 250 | fs.mu.Unlock() 251 | if err == afero.ErrFileNotFound && flag&os.O_CREATE != 0 { 252 | return fs.Create(name) 253 | } 254 | entry := fs.findEntry(name) 255 | if fd != nil && entry != nil { 256 | SetMode(fd, perm) 257 | file := NewFileHandle(fd, fs, *entry) 258 | if flag&os.O_APPEND > 0 { 259 | _, err := file.Seek(0, os.SEEK_END) 260 | if err != nil { 261 | file.Close() 262 | return nil, err 263 | } 264 | } 265 | return file, nil 266 | } 267 | return nil, err 268 | } 269 | 270 | // Remove removes a file identified by name, returning an error, if any 271 | // happens. 272 | func (fs *githubFs) Remove(name string) error { 273 | fs.mu.Lock() 274 | defer fs.mu.Unlock() 275 | return fs.remove(name) 276 | } 277 | 278 | func (fs *githubFs) remove(name string) error { 279 | normalName := strings.TrimPrefix(name, "/") 280 | entry := fs.findEntry(name) 281 | if entry == nil { 282 | return afero.ErrFileNotFound 283 | } 284 | resp, _, err := fs.client.Repositories.DeleteFile(context.TODO(), fs.user, fs.repo, normalName, &github.RepositoryContentFileOptions{ 285 | Message: String(CommitMessage), 286 | SHA: String(entry.GetSHA()), 287 | Branch: String(fs.branch.GetName()), 288 | }) 289 | if err != nil { 290 | return err 291 | } 292 | return fs.updateTree(resp.Tree.GetSHA()) 293 | } 294 | 295 | // RemoveAll removes a directory path and any children it contains. It 296 | // does not fail if the path does not exist (return nil). 297 | func (fs *githubFs) RemoveAll(path string) error { 298 | fs.mu.Lock() 299 | defer fs.mu.Unlock() 300 | normalName := strings.TrimSuffix(strings.TrimPrefix(path, "/"), "/") 301 | entry := fs.findEntry(normalName) 302 | if entry == nil { 303 | return afero.ErrFileNotFound 304 | } 305 | if entry.GetType() == "blob" { 306 | return fs.remove(path) 307 | } 308 | // TODO: remove all files in a single commit 309 | for _, e := range fs.tree.Entries { 310 | if e.GetType() == "tree" { 311 | continue 312 | } 313 | if strings.HasPrefix(e.GetPath(), normalName+"/") { 314 | err := fs.remove(e.GetPath()) 315 | if err != nil { 316 | return err 317 | } 318 | } 319 | } 320 | return nil 321 | } 322 | 323 | // Rename renames a file. 324 | func (fs *githubFs) Rename(oldname, newname string) error { 325 | fs.mu.Lock() 326 | defer fs.mu.Unlock() 327 | normalOld := strings.TrimPrefix(oldname, "/") 328 | normalNew := strings.TrimPrefix(newname, "/") 329 | for i, e := range fs.tree.Entries { 330 | if e.GetPath() == normalOld { 331 | fs.tree.Entries[i].Path = String(normalNew) 332 | } 333 | } 334 | return fs.commit() 335 | } 336 | 337 | func (fs *githubFs) updateBranch() (err error) { 338 | fs.branch, _, err = fs.client.Repositories.GetBranch(context.TODO(), fs.user, fs.repo, fs.branch.GetName()) 339 | return 340 | } 341 | 342 | func (fs *githubFs) commit() error { 343 | // TODO: can we do this with less requests? 344 | branch, _, err := fs.client.Repositories.GetBranch(context.TODO(), fs.user, fs.repo, fs.branch.GetName()) 345 | if err != nil { 346 | return err 347 | } 348 | // TODO: can we remove this constraint? make it more like user workflow? 349 | if branch.GetCommit().GetSHA() != fs.branch.GetCommit().GetSHA() { 350 | return errors.New("commits have been made since last filesystem operation") 351 | } 352 | fs.branch = branch 353 | 354 | tree, _, err := fs.client.Git.CreateTree(context.TODO(), fs.user, fs.repo, "", fs.tree.Entries) 355 | if err != nil { 356 | return err 357 | } 358 | err = fs.updateTree(tree.GetSHA()) 359 | if err != nil { 360 | return err 361 | } 362 | 363 | commit, _, err := fs.client.Git.CreateCommit(context.TODO(), fs.user, fs.repo, &github.Commit{ 364 | Message: String(CommitMessage), 365 | Tree: fs.tree, 366 | Parents: []github.Commit{{SHA: fs.branch.GetCommit().SHA}}, 367 | }) 368 | if err != nil { 369 | return err 370 | } 371 | _, _, err = fs.client.Git.UpdateRef(context.TODO(), fs.user, fs.repo, &github.Reference{ 372 | Ref: String("heads/" + fs.branch.GetName()), 373 | Object: &github.GitObject{ 374 | SHA: commit.SHA, 375 | }, 376 | }, false) 377 | if err != nil { 378 | return err 379 | } 380 | return fs.updateBranch() 381 | } 382 | 383 | // Stat returns a FileInfo describing the named file, or an error, if any 384 | // happens. 385 | func (fs *githubFs) Stat(name string) (os.FileInfo, error) { 386 | // TODO: properly 387 | f, err := fs.Open(name) 388 | if err != nil { 389 | return nil, err 390 | } 391 | return f.Stat() 392 | } 393 | 394 | // The name of this FileSystem 395 | func (fs *githubFs) Name() string { 396 | return "github-api" 397 | } 398 | 399 | //Chmod changes the mode of the named file to mode. 400 | func (fs *githubFs) Chmod(name string, mode os.FileMode) error { 401 | // TODO: NOT YET IMPLEMENTED 402 | return nil 403 | } 404 | 405 | //Chtimes changes the access and modification times of the named file 406 | func (fs *githubFs) Chtimes(name string, atime time.Time, mtime time.Time) error { 407 | // no-op 408 | return nil 409 | } 410 | --------------------------------------------------------------------------------