issues

Github, Jira, Gitlab issues from Plan 9's acme
Log | Files | Refs | README | LICENSE

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 }