fs.go (8592B)
1 package jira 2 3 import ( 4 "fmt" 5 "io" 6 "io/fs" 7 "os" 8 "path" 9 "strings" 10 "sync" 11 "time" 12 ) 13 14 type FS struct { 15 Client *Client 16 root *fid 17 cache *dirCache 18 } 19 20 const ( 21 ftypeRoot int = iota 22 ftypeProject 23 ftypeIssue 24 ftypeIssueDir 25 ftypeComment 26 ) 27 28 type fid struct { 29 *Client 30 name string 31 typ int 32 rd io.Reader 33 parent *fid 34 35 // May be set but only as an optimisation to skip a Stat(). 36 stat fs.FileInfo 37 38 // directories only 39 children []fs.DirEntry 40 dirp int 41 } 42 43 func (f *fid) Name() string { return f.name } 44 func (f *fid) IsDir() bool { return f.Type().IsDir() } 45 46 func (f *fid) Type() fs.FileMode { 47 switch f.typ { 48 case ftypeRoot, ftypeProject, ftypeIssueDir: 49 return fs.ModeDir 50 } 51 return 0 52 } 53 54 func (f *fid) Info() (fs.FileInfo, error) { return f.Stat() } 55 56 func (f *fid) Stat() (fs.FileInfo, error) { 57 if f.Client.Debug { 58 fmt.Fprintln(os.Stderr, "stat", f.Name()) 59 } 60 if f.stat != nil { 61 return f.stat, nil 62 } 63 64 switch f.typ { 65 case ftypeRoot: 66 return &stat{".", int64(len(f.children)), 0o444 | fs.ModeDir, time.Time{}}, nil 67 case ftypeProject: 68 p, err := f.Project(f.name) 69 if err != nil { 70 return nil, &fs.PathError{"stat", f.name, err} 71 } 72 return p, nil 73 case ftypeIssueDir, ftypeIssue: 74 is, err := f.Issue(f.issueKey()) 75 if err != nil { 76 return nil, &fs.PathError{"stat", f.name, err} 77 } 78 if f.typ == ftypeIssueDir { 79 f.children = issueChildren(f, is) 80 return is, nil 81 } 82 // optimisation: we might read the file soon so load the contents. 83 f.rd = strings.NewReader(printIssue(is)) 84 return &stat{f.name, int64(len(printIssue(is))), 0o444, is.Updated}, nil 85 case ftypeComment: 86 c, err := f.Comment(f.issueKey(), f.name) 87 if err != nil { 88 return nil, &fs.PathError{"stat", f.name, err} 89 } 90 // optimisation: we might read the file soon so load the contents. 91 f.rd = strings.NewReader(printComment(c)) 92 return c, nil 93 } 94 err := fmt.Errorf("unexpected fid type %d", f.typ) 95 return nil, &fs.PathError{"stat", f.name, err} 96 } 97 98 func (f *fid) Read(p []byte) (n int, err error) { 99 if f.Client.Debug { 100 fmt.Fprintln(os.Stderr, "read", f.Name()) 101 } 102 if f.rd == nil { 103 switch f.typ { 104 case ftypeComment: 105 c, err := f.Comment(f.issueKey(), f.name) 106 if err != nil { 107 err = fmt.Errorf("get comment %s: %w", f.issueKey(), err) 108 return 0, &fs.PathError{"read", f.name, err} 109 } 110 f.rd = strings.NewReader(printComment(c)) 111 case ftypeIssue: 112 is, err := f.Issue(f.issueKey()) 113 if err != nil { 114 err = fmt.Errorf("get issue %s: %w", f.issueKey(), err) 115 return 0, &fs.PathError{"read", f.name, err} 116 } 117 f.rd = strings.NewReader(printIssue(is)) 118 default: 119 var err error 120 if f.children == nil { 121 f.children, err = f.ReadDir(-1) 122 if err != nil { 123 return 0, &fs.PathError{"read", f.name, err} 124 } 125 } 126 buf := &strings.Builder{} 127 for _, d := range f.children { 128 fmt.Fprintln(buf, fs.FormatDirEntry(d)) 129 } 130 f.rd = strings.NewReader(buf.String()) 131 } 132 } 133 return f.rd.Read(p) 134 } 135 136 func (f *fid) Close() error { 137 f.rd = nil 138 f.stat = nil 139 return nil 140 } 141 142 func (f *fid) ReadDir(n int) ([]fs.DirEntry, error) { 143 if f.Client.Debug { 144 fmt.Fprintln(os.Stderr, "readdir", f.Name()) 145 } 146 if !f.IsDir() { 147 return nil, fmt.Errorf("not a directory") 148 } 149 if f.children == nil { 150 switch f.typ { 151 case ftypeRoot: 152 return nil, fmt.Errorf("root initialised incorrectly: no dir entries") 153 case ftypeProject: 154 issues, err := f.Issues(f.name) 155 if err != nil { 156 return nil, fmt.Errorf("get issues: %w", err) 157 } 158 f.children = make([]fs.DirEntry, len(issues)) 159 for i, issue := range issues { 160 f.children[i] = &fid{ 161 Client: f.Client, 162 name: issue.Name(), 163 typ: ftypeIssueDir, 164 parent: f, 165 } 166 } 167 case ftypeIssueDir: 168 issue, err := f.Issue(f.issueKey()) 169 if err != nil { 170 return nil, fmt.Errorf("get issue %s: %w", f.name, err) 171 } 172 f.children = issueChildren(f, issue) 173 } 174 } 175 176 if f.dirp >= len(f.children) { 177 if n <= 0 { 178 return nil, nil 179 } 180 return nil, io.EOF 181 } 182 if n <= 0 { 183 f.dirp = len(f.children) 184 return f.children, nil 185 } 186 187 var err error 188 d := f.children[f.dirp:] 189 if len(d) >= n { 190 d = d[:n] 191 } else if len(d) <= n { 192 err = io.EOF 193 } 194 f.dirp += n 195 return d, err 196 } 197 198 func issueChildren(parent *fid, is *Issue) []fs.DirEntry { 199 kids := make([]fs.DirEntry, len(is.Comments)+1) 200 for i, c := range is.Comments { 201 kids[i] = &fid{ 202 Client: parent.Client, 203 name: c.ID, 204 typ: ftypeComment, 205 rd: strings.NewReader(printComment(&is.Comments[i])), 206 parent: parent, 207 stat: &is.Comments[i], 208 } 209 } 210 kids[len(kids)-1] = &fid{ 211 name: "issue", 212 Client: parent.Client, 213 typ: ftypeIssue, 214 rd: strings.NewReader(is.Summary), 215 parent: parent, 216 stat: &stat{"issue", int64(len(printIssue(is))), 0o444, is.Updated}, 217 } 218 return kids 219 } 220 221 func (f *fid) issueKey() string { 222 // to make the issue key e.g. "EXAMPLE-42" 223 // we need the name of the issue (parent name, "42") 224 // and the name of the project (the issue's parent's name, "EXAMPLE") 225 var project, issueNumber string 226 switch f.typ { 227 default: 228 return "" 229 case ftypeComment, ftypeIssue: 230 project = f.parent.parent.name 231 issueNumber = f.parent.name 232 case ftypeIssueDir: 233 project = f.parent.name 234 issueNumber = f.name 235 } 236 return project + "-" + issueNumber 237 } 238 239 func (fsys *FS) Open(name string) (fs.File, error) { 240 if !fs.ValidPath(name) { 241 return nil, &fs.PathError{"open", name, fs.ErrInvalid} 242 } 243 name = path.Clean(name) 244 if strings.Contains(name, "\\") { 245 return nil, fs.ErrNotExist 246 } 247 248 if fsys.root == nil { 249 var err error 250 fsys.root, err = makeRoot(fsys.Client) 251 if err != nil { 252 return nil, fmt.Errorf("make root file: %w", err) 253 } 254 } 255 if fsys.cache == nil { 256 var err error 257 fsys.cache = &dirCache{} 258 fsys.cache.root, err = makeRoot(fsys.Client) 259 if err != nil { 260 return nil, fmt.Errorf("make cache root file: %w", err) 261 } 262 } 263 264 if fsys.Client.Debug { 265 fmt.Fprintln(os.Stderr, "open", name) 266 } 267 268 if name == "." { 269 f := *fsys.root 270 return &f, nil 271 } 272 273 elems := strings.Split(name, "/") 274 if elems[0] == "." && len(elems) > 1 { 275 elems = elems[1:] 276 } 277 278 if f := fsys.cache.lookup(elems); f != nil { 279 if fsys.Client.Debug { 280 fmt.Fprintln(os.Stderr, "cachehit", name) 281 } 282 g := *f 283 g.children = nil 284 return &g, nil 285 } 286 287 f := fsys.root 288 for i, elem := range elems { 289 dir, err := find(f, elem) 290 if err != nil { 291 return nil, &fs.PathError{"open", name, err} 292 } 293 f = dir 294 fsys.cache.put(elems[:i], *dir) 295 } 296 g := *f 297 return &g, nil 298 } 299 300 func makeRoot(client *Client) (*fid, error) { 301 projects, err := client.Projects() 302 if err != nil { 303 return nil, err 304 } 305 root := &fid{ 306 Client: client, 307 name: ".", 308 typ: ftypeRoot, 309 children: make([]fs.DirEntry, len(projects)), 310 } 311 for i, p := range projects { 312 root.children[i] = &fid{ 313 Client: client, 314 name: p.Key, 315 typ: ftypeProject, 316 } 317 } 318 return root, nil 319 } 320 321 type dirCache struct { 322 mu sync.Mutex 323 root *fid 324 } 325 326 func (cache *dirCache) lookup(wnames []string) *fid { 327 cache.mu.Lock() 328 defer cache.mu.Unlock() 329 330 next := cache.root 331 for _, name := range wnames { 332 match := qfind(next, name) 333 if match == nil { 334 return nil 335 } 336 next = match 337 } 338 return next 339 } 340 341 func (cache *dirCache) put(wnames []string, f fid) (ok bool) { 342 parent := cache.lookup(wnames) 343 if parent == nil { 344 return false 345 } 346 347 cache.mu.Lock() 348 defer cache.mu.Unlock() 349 parent.children = append(parent.children, &f) 350 return true 351 } 352 353 func qfind(dir *fid, name string) *fid { 354 if !dir.IsDir() { 355 return nil 356 } 357 for _, d := range dir.children { 358 if d.Name() == name { 359 return d.(*fid) 360 } 361 } 362 return nil 363 } 364 365 func find(dir *fid, name string) (*fid, error) { 366 if !dir.IsDir() { 367 return nil, fs.ErrNotExist 368 } 369 child := &fid{Client: dir.Client, parent: dir} 370 switch dir.typ { 371 case ftypeRoot: 372 for _, d := range dir.children { 373 if d.Name() == name { 374 child, ok := d.(*fid) 375 if !ok { 376 return nil, fmt.Errorf("unexpected dir entry type %T", d) 377 } 378 return child, nil 379 } 380 } 381 return nil, fs.ErrNotExist 382 case ftypeProject: 383 key := fmt.Sprintf("%s-%s", dir.name, name) 384 ok, err := dir.CheckIssue(key) 385 if err != nil { 386 return nil, err 387 } 388 if !ok { 389 return nil, fs.ErrNotExist 390 } 391 child.name = name 392 child.typ = ftypeIssueDir 393 return child, nil 394 case ftypeIssueDir: 395 if name == "issue" { 396 child.name = name 397 child.typ = ftypeIssue 398 return child, nil 399 } 400 ok, err := dir.checkComment(dir.issueKey(), name) 401 if err != nil { 402 return nil, err 403 } else if !ok { 404 return nil, fs.ErrNotExist 405 } 406 child.name = name 407 child.typ = ftypeComment 408 return child, nil 409 } 410 return nil, fs.ErrNotExist 411 }