1
2
3
4
5
6
7
8
9 package ghfs
10
11 import (
12 "context"
13 "fmt"
14 "io"
15 "io/fs"
16 "time"
17
18 "github.com/google/go-github/v47/github"
19 )
20
21
22
23 type FS struct {
24 fetcher
25 }
26
27 var (
28 _ fs.FS = &FS{}
29 _ fs.ReadDirFS = &FS{}
30 _ fs.ReadFileFS = &FS{}
31 )
32
33 type File struct {
34 rc io.ReadCloser
35 i *info
36 }
37
38 var _ fs.File = &File{}
39
40 func (f *File) Stat() (fs.FileInfo, error) { return f.i, nil }
41 func (f *File) Read(b []byte) (int, error) { return f.rc.Read(b) }
42 func (f *File) Close() error { return f.rc.Close() }
43
44 type Dir struct {
45 i *info
46 files []info
47 offset int
48 }
49
50 var (
51 _ fs.File = &Dir{}
52 _ fs.ReadDirFile = &Dir{}
53 )
54
55 func (d *Dir) Stat() (fs.FileInfo, error) { return d.i, nil }
56 func (d *Dir) Close() error { return nil }
57 func (d *Dir) Read(_ []byte) (int, error) {
58 return 0, &fs.PathError{Op: "read", Path: d.i.name, Err: fmt.Errorf("is a directory")}
59 }
60
61 func (d *Dir) ReadDir(count int) ([]fs.DirEntry, error) {
62 n := len(d.files) - d.offset
63 if n == 0 {
64
65 if count <= 0 {
66 return nil, nil
67 }
68
69 return nil, io.EOF
70 }
71
72 if count > 0 && n > count {
73 n = count
74 }
75
76 list := make([]fs.DirEntry, n)
77 for i := range list {
78 list[i] = &d.files[d.offset+i]
79 }
80
81 d.offset += n
82 return list, nil
83 }
84
85
86
87
88 type info struct {
89 size int64
90 mode fs.FileMode
91 name string
92 }
93
94 var _ fs.FileInfo = &info{}
95
96 func (i *info) Sys() any { return nil }
97 func (i *info) IsDir() bool { return i.mode == fs.ModeDir }
98 func (i *info) Name() string { return i.name }
99 func (i *info) Size() int64 { return i.size }
100 func (i *info) Mode() fs.FileMode { return i.mode }
101 func (i *info) ModTime() time.Time { return time.Unix(0, 0) }
102
103 var _ fs.DirEntry = &info{}
104
105 func (i *info) Type() fs.FileMode { return i.Mode().Type() }
106 func (i *info) Info() (fs.FileInfo, error) { return i, nil }
107
108 type RefNotFoundError struct {
109 Owner string
110 Repo string
111 Ref string
112 }
113
114 func (rnfe RefNotFoundError) Error() string {
115 return fmt.Sprintf("the requested ref %s was not found at ", rnfe.Ref)
116 }
117
118
119
120
121
122
123
124
125
126 func New(ctx context.Context, gh *github.Client, owner, repo, ref string) (*FS, error) {
127 _, resp, err := gh.Repositories.GetBranch(ctx, owner, repo, ref, false)
128 if err != nil {
129 if resp.StatusCode == 404 {
130 return nil, RefNotFoundError{Owner: owner, Repo: repo, Ref: ref}
131 }
132 return nil, err
133 }
134
135 return &FS{fetcher: &ghFetcher{gh, ctx, owner, repo, ref}}, nil
136 }
137
138
139
140
141 func (f *FS) Open(name string) (fs.File, error) {
142 file, dir, err := f.getContents(name)
143 if err != nil {
144 return nil, err
145 }
146
147 switch {
148 case file != nil:
149 rc, err := f.downloadContents(file)
150 if err != nil {
151 return nil, err
152 }
153 return &File{rc, infoFromRepoContent(file)}, nil
154 case dir != nil:
155 d := &Dir{files: make([]info, 0, len(dir)-1), offset: 0}
156 for _, c := range dir {
157 if c.GetPath() == name {
158 d.i = infoFromRepoContent(c)
159 continue
160 }
161 d.files = append(d.files, *infoFromRepoContent(c))
162 }
163 return d, nil
164 default:
165 return nil, fmt.Errorf("unexpected state, either file or directory " +
166 "contents should have been returned")
167 }
168 }
169
170
171 func (f *FS) ReadDir(name string) ([]fs.DirEntry, error) {
172 _, dir, err := f.getContents(name)
173 if err != nil {
174 return nil, err
175 }
176 if dir == nil {
177 return nil, fmt.Errorf("%s is not a directory", name)
178 }
179
180 entries := make([]fs.DirEntry, 0, len(dir)-1)
181 for _, c := range dir {
182
183 if c.GetPath() == name {
184 continue
185 }
186 entries = append(entries, infoFromRepoContent(c))
187 }
188
189 return entries, nil
190 }
191
192
193 func (f *FS) ReadFile(name string) ([]byte, error) {
194 file, _, err := f.getContents(name)
195 if err != nil {
196 return nil, err
197 }
198
199 if file == nil {
200 return nil, fmt.Errorf("unexpected state, file should have been returned")
201 }
202
203 rc, err := f.downloadContents(file)
204 if err != nil {
205 return nil, err
206 }
207 defer rc.Close()
208
209
210 fSize := file.GetSize()
211 fSize++
212
213 if fSize < 512 {
214 fSize = 512
215 }
216 data := make([]byte, 0, fSize)
217 for {
218 if len(data) >= cap(data) {
219 d := append(data[:cap(data)], 0)
220 data = d[:len(data)]
221 }
222 n, err := rc.Read(data[len(data):cap(data)])
223 data = data[:len(data)+n]
224 if err != nil {
225 if err == io.EOF {
226 err = nil
227 }
228 return data, err
229 }
230 }
231 }
232
233 func infoFromRepoContent(content *github.RepositoryContent) *info {
234 i := &info{
235 size: int64(content.GetSize()),
236 name: content.GetName(),
237 }
238
239 switch {
240 case content.GetTarget() != "":
241 i.mode = fs.ModeSymlink
242 case content.GetType() == "dir":
243 i.mode = fs.ModeDir
244 case content.GetType() == "file" || content.GetType() == "submodule":
245 i.mode = fs.FileMode(0)
246 }
247
248 return i
249 }
250
View as plain text