ref: 1d43766c7ccd703adf2015aefc18609748b6f5eb
author: Kenny Levinsen <kl@codesealer.com>
date: Mon Jun 6 08:46:30 EDT 2016
Initial commit
--- /dev/null
+++ b/jira.go
@@ -1,0 +1,405 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "log"
+ "strconv"
+ "strings"
+ "sync"
+
+ "github.com/andygrunwald/go-jira"
+ "github.com/joushou/qp"
+ "github.com/joushou/qptools/fileserver/trees"
+)
+
+type jiraWalker interface {
+ Walk(jc *jira.Client, name string) (trees.File, error)
+}
+
+type jiraLister interface {
+ List(jc *jira.Client) ([]qp.Stat, error)
+}
+
+type jiraRemover interface {
+ Remove(jc *jira.Client, name string) error
+}
+
+type CommentView struct {
+ project string
+ issueNo string
+}
+
+func (cw *CommentView) Walk(jc *jira.Client, name string) (trees.File, error) {
+ switch name {
+ case "comment":
+ sf := trees.NewSyntheticFile(name, 0777, "jira", "jira")
+ onClose := func() error {
+ sf.Lock()
+ body := string(sf.Content)
+ sf.Unlock()
+
+ return AddComment(jc, fmt.Sprintf("%s-%s", cw.project, cw.issueNo), body)
+ }
+ return NewCloseSaver(sf, onClose), nil
+ default:
+ cmt, err := GetComment(jc, fmt.Sprintf("%s-%s", cw.project, cw.issueNo), name)
+ if err != nil {
+ return nil, err
+ }
+ if len(cmt.Body) > 0 && cmt.Body[len(cmt.Body)-1] != '\n' {
+ cmt.Body += "\n"
+ }
+
+ sf := trees.NewSyntheticFile(name, 0777, cmt.Author.Name, "jira")
+ sf.SetContent([]byte(cmt.Body))
+
+ onClose := func() error {
+ sf.Lock()
+ body := string(sf.Content)
+ sf.Unlock()
+
+ return SetComment(jc, fmt.Sprintf("%s-%s", cw.project, cw.issueNo), name, body)
+ }
+
+ return NewCloseSaver(sf, onClose), nil
+ }
+}
+
+func (cw *CommentView) List(jc *jira.Client) ([]qp.Stat, error) {
+ strs, err := GetCommentsForIssue(jc, fmt.Sprintf("%s-%s", cw.project, cw.issueNo))
+ if err != nil {
+ return nil, err
+ }
+
+ strs = append(strs, "comment")
+
+ return StringsToStats(strs, 0777, "jira", "jira"), nil
+}
+
+func (cw *CommentView) Remove(jc *jira.Client, name string) error {
+ switch name {
+ case "comment":
+ return trees.ErrPermissionDenied
+ default:
+ return RemoveComment(jc, fmt.Sprintf("%s-%s", cw.project, cw.issueNo), name)
+ }
+}
+
+type IssueView struct {
+ project string
+ issueNo string
+
+ issueLock sync.Mutex
+ newIssue bool
+ values map[string]string
+}
+
+func (iw *IssueView) normalFiles() (files, dirs []string) {
+ files = []string{"assignee", "creator", "ctl", "description", "issuetype", "key", "reporter", "status", "summary", "labels", "transitions"}
+ dirs = []string{"comments"}
+ return
+}
+
+func (iw *IssueView) newFiles() (files, dirs []string) {
+ files = []string{"ctl", "description", "issuetype", "summary"}
+ return
+}
+
+func (iw *IssueView) newWalk(jc *jira.Client, file string) (trees.File, error) {
+ files, dirs := iw.newFiles()
+ if !StringExistsInSets(file, files, dirs) {
+ return nil, nil
+ }
+
+ switch file {
+ case "ctl":
+ cmds := map[string]func([]string) error{
+ "commit": func(args []string) error {
+ var issuetype, summary, description string
+
+ iw.issueLock.Lock()
+ isNew := iw.newIssue
+ if iw.values != nil {
+ issuetype = strings.Replace(string(iw.values["issuetype"]), "\n", "", -1)
+ summary = strings.Replace(string(iw.values["summary"]), "\n", "", -1)
+ description = strings.Replace(string(iw.values["description"]), "\n", "", -1)
+ }
+ iw.issueLock.Unlock()
+
+ if !isNew {
+ return errors.New("issue already committed")
+ }
+
+ issue := jira.Issue{
+ Fields: &jira.IssueFields{
+ Type: jira.IssueType{
+ Name: issuetype,
+ },
+ Project: jira.Project{
+ Key: iw.project,
+ },
+ Summary: summary,
+ Description: description,
+ },
+ }
+
+ key, err := CreateIssue(jc, &issue)
+ if err != nil {
+ log.Printf("Create failed: %v", err)
+ return err
+ }
+
+ keys := strings.Split(key, "-")
+ if len(keys) != 2 {
+ log.Printf("Weird key: %s", keys)
+ return errors.New("weird key")
+ }
+ iw.issueLock.Lock()
+ iw.issueNo = keys[1]
+ iw.newIssue = false
+ iw.issueLock.Unlock()
+ return nil
+ },
+ }
+ return NewCommandFile("ctl", 0777, "jira", "jira", cmds), nil
+ default:
+ sf := trees.NewSyntheticFile(file, 0777, "jira", "jira")
+ iw.issueLock.Lock()
+ defer iw.issueLock.Unlock()
+
+ if iw.values == nil {
+ iw.values = make(map[string]string)
+ }
+
+ value := iw.values[file]
+
+ sf.SetContent([]byte(value))
+
+ onClose := func() error {
+ iw.issueLock.Lock()
+ defer iw.issueLock.Unlock()
+
+ iw.values[file] = string(sf.Content)
+ return nil
+ }
+
+ return NewCloseSaver(sf, onClose), nil
+ }
+
+}
+
+func (iw *IssueView) normalWalk(jc *jira.Client, file string) (trees.File, error) {
+ files, dirs := iw.normalFiles()
+ if !StringExistsInSets(file, files, dirs) {
+ return nil, nil
+ }
+
+ issue, err := GetIssue(jc, fmt.Sprintf("%s-%s", iw.project, iw.issueNo))
+ if err != nil {
+ return nil, err
+ }
+
+ sf := trees.NewSyntheticFile(file, 0777, "jira", "jira")
+
+ switch file {
+ case "assignee":
+ if issue.Fields != nil && issue.Fields.Assignee != nil {
+ sf.SetContent([]byte(issue.Fields.Assignee.Name + "\n"))
+ }
+ case "reporter":
+ if issue.Fields != nil && issue.Fields.Reporter != nil {
+ sf.SetContent([]byte(issue.Fields.Reporter.Name + "\n"))
+ }
+ case "creator":
+ if issue.Fields != nil && issue.Fields.Creator != nil {
+ sf.SetContent([]byte(issue.Fields.Creator.Name + "\n"))
+ }
+ case "summary":
+ if issue.Fields != nil {
+ sf.SetContent([]byte(issue.Fields.Summary + "\n"))
+ }
+ case "description":
+ if issue.Fields != nil {
+ sf.SetContent([]byte(issue.Fields.Description + "\n"))
+ }
+ case "issuetype":
+ if issue.Fields != nil {
+ sf.SetContent([]byte(issue.Fields.Type.Name + "\n"))
+ }
+ case "status":
+ if issue.Fields != nil && issue.Fields.Status != nil {
+ sf.SetContent([]byte(issue.Fields.Status.Name + "\n"))
+ }
+ case "key":
+ sf.SetContent([]byte(issue.Key + "\n"))
+ case "labels":
+ if issue.Fields != nil {
+ var s string
+ for _, lbl := range issue.Fields.Labels {
+ s += lbl + "\n"
+ }
+ sf.SetContent([]byte(s))
+ }
+ case "transitions":
+ trs, err := GetTransitionsForIssue(jc, issue.Key)
+ if err != nil {
+ log.Printf("Could not get transitions for issue %s: %v", issue.Key, err)
+ return nil, err
+ }
+
+ var s string
+ for _, tr := range trs {
+ s += tr.Name + "\n"
+ }
+ sf.SetContent([]byte(s))
+ case "comments":
+ return NewJiraDir(file,
+ 0555|qp.DMDIR,
+ "jira",
+ "jira",
+ jc,
+ &CommentView{project: iw.project, issueNo: iw.issueNo})
+ case "ctl":
+ cmds := map[string]func([]string) error{
+ "delete": func(args []string) error {
+ return DeleteIssue(jc, issue.Key)
+ },
+ }
+ return NewCommandFile("ctl", 0777, "jira", "jira", cmds), nil
+
+ }
+
+ onClose := func() error {
+ switch file {
+ case "key":
+ return nil
+ case "status", "transitions":
+ sf.Lock()
+ str := string(sf.Content)
+ sf.Unlock()
+ str = strings.Replace(str, "\n", "", -1)
+
+ return TransitionIssue(jc, issue.Key, str)
+ default:
+ sf.Lock()
+ str := string(sf.Content)
+ sf.Unlock()
+ if file != "description" && file != "labels" {
+ str = strings.Replace(str, "\n", "", -1)
+ }
+ return SetFieldInIssue(jc, issue.Key, file, str)
+ }
+ }
+
+ return NewCloseSaver(sf, onClose), nil
+}
+
+func (iw *IssueView) Walk(jc *jira.Client, file string) (trees.File, error) {
+ iw.issueLock.Lock()
+ isNew := iw.newIssue
+ iw.issueLock.Unlock()
+
+ if isNew {
+ return iw.newWalk(jc, file)
+ } else {
+ return iw.normalWalk(jc, file)
+ }
+}
+
+func (iw *IssueView) List(jc *jira.Client) ([]qp.Stat, error) {
+ iw.issueLock.Lock()
+ isNew := iw.newIssue
+ iw.issueLock.Unlock()
+
+ var files, dirs []string
+ if isNew {
+ files, dirs = iw.newFiles()
+ } else {
+ files, dirs = iw.normalFiles()
+ }
+ var stats []qp.Stat
+
+ stats = append(stats, StringsToStats(files, 0777, "jira", "jira")...)
+ stats = append(stats, StringsToStats(dirs, 0777|qp.DMDIR, "jira", "jira")...)
+
+ return stats, nil
+}
+
+type ProjectView struct {
+ project string
+}
+
+func (pw *ProjectView) Walk(jc *jira.Client, issueNo string) (trees.File, error) {
+ iw := &IssueView{
+ project: pw.project,
+ }
+
+ if issueNo == "new" {
+ iw.newIssue = true
+ } else {
+ // Check if the thing is a valid issue number.
+ if _, err := strconv.ParseUint(issueNo, 10, 64); err != nil {
+ return nil, nil
+ }
+
+ _, err := GetIssue(jc, fmt.Sprintf("%s-%s", pw.project, issueNo))
+ if err != nil {
+ return nil, err
+ }
+ iw.issueNo = issueNo
+ }
+
+ return NewJiraDir(issueNo, 0555|qp.DMDIR, "jira", "jira", jc, iw)
+}
+
+func (pw *ProjectView) List(jc *jira.Client) ([]qp.Stat, error) {
+ keys, err := GetKeysForNIssues(jc, pw.project, 250)
+ if err != nil {
+ return nil, err
+ }
+
+ keys = append(keys, "new")
+ return StringsToStats(keys, 0555|qp.DMDIR, "jira", "jira"), nil
+}
+
+type JiraView struct{}
+
+func (jw *JiraView) Walk(jc *jira.Client, projectName string) (trees.File, error) {
+ projectName = strings.ToUpper(projectName)
+ projects, err := GetProjects(jc)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, project := range projects {
+ if project.Key == projectName {
+ goto found
+ }
+ }
+
+ return nil, nil
+
+found:
+
+ return NewJiraDir(projectName,
+ 0555|qp.DMDIR,
+ "jira",
+ "jira",
+ jc,
+ &ProjectView{project: projectName})
+}
+
+func (jw *JiraView) List(jc *jira.Client) ([]qp.Stat, error) {
+ projects, err := GetProjects(jc)
+ if err != nil {
+ return nil, err
+ }
+
+ var strs []string
+ for _, p := range projects {
+ strs = append(strs, p.Key)
+ }
+
+ return StringsToStats(strs, 0555|qp.DMDIR, "jira", "jira"), nil
+}
--- /dev/null
+++ b/main.go
@@ -1,0 +1,66 @@
+package main
+
+import (
+ "fmt"
+ "net"
+ "os"
+
+ "github.com/andygrunwald/go-jira"
+ "github.com/howeyc/gopass"
+ "github.com/joushou/qp"
+ "github.com/joushou/qptools/fileserver"
+)
+
+func main() {
+ jiraClient, err := jira.NewClient(nil, os.Args[1])
+ if err != nil {
+ fmt.Printf("Could not connect to JIRA: %v\n", err)
+ return
+ }
+
+ var user, password string
+ fmt.Printf("Username: ")
+ _, err = fmt.Scan(&user)
+ if err != nil {
+ fmt.Printf("Could not read username: %v", err)
+ return
+ }
+ fmt.Printf("Password: ")
+ pass, err := gopass.GetPasswdMasked()
+ if err != nil {
+ fmt.Printf("Could not read password: %v", err)
+ return
+ }
+ password = string(pass)
+
+ res, err := jiraClient.Authentication.AcquireSessionCookie(user, password)
+ if err != nil || res == false {
+ fmt.Printf("Could not authenticate to JIRA: %v\n", err)
+ return
+ }
+
+ root, err := NewJiraDir("", 0555|qp.DMDIR, "jira", "jira", jiraClient, &JiraView{})
+ if err != nil {
+ fmt.Printf("Could not create JIRA view")
+ return
+ }
+
+ l, err := net.Listen("tcp", ":30000")
+ if err != nil {
+ fmt.Printf("Could not listen: %v\n", err)
+ return
+ }
+
+ for {
+ conn, err := l.Accept()
+ if err != nil {
+ fmt.Printf("Accept failed: %v\n", err)
+ return
+ }
+
+ f := fileserver.New(conn, root, nil)
+ f.Verbosity = fileserver.Quiet
+ go f.Serve()
+ }
+
+}
--- /dev/null
+++ b/utils.go
@@ -1,0 +1,514 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "log"
+ "net/http/httputil"
+ "strings"
+ "time"
+
+ "github.com/andygrunwald/go-jira"
+ "github.com/joushou/qp"
+ "github.com/joushou/qptools/fileserver/trees"
+)
+
+type SearchResult struct {
+ Issues []jira.Issue `json:"issues"`
+}
+
+func GetProjects(jc *jira.Client) ([]jira.Project, error) {
+ req, err := jc.NewRequest("GET", "/rest/api/2/project", nil)
+ if err != nil {
+ return nil, fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ var projects []jira.Project
+ if _, err := jc.Do(req, &projects); err != nil {
+ return nil, fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ return projects, nil
+}
+
+func GetTypesForProject(jc *jira.Client, project string) ([]string, error) {
+ req, err := jc.NewRequest("GET", "/rest/api/2/issuetype", nil)
+ if err != nil {
+ return nil, fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ var types []jira.IssueType
+ if _, err := jc.Do(req, &types); err != nil {
+ return nil, fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ ss := make([]string, len(types))
+ for i, tp := range types {
+ ss[i] = tp.Name
+ }
+
+ return ss, nil
+}
+
+func GetKeysForNIssues(jc *jira.Client, project string, n int) ([]string, error) {
+ cmd := fmt.Sprintf("/rest/api/2/search?fields=key&maxResults=%d&jql=project=%s", n, project)
+
+ req, err := jc.NewRequest("GET", cmd, nil)
+ if err != nil {
+ return nil, fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ var s SearchResult
+ if _, err := jc.Do(req, &s); err != nil {
+ return nil, fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ ss := make([]string, len(s.Issues))
+ for i, issue := range s.Issues {
+ s := strings.Split(issue.Key, "-")
+ if len(s) != 2 {
+ continue
+ }
+ ss[i] = s[1]
+ }
+
+ return ss, nil
+}
+
+func GetIssue(jc *jira.Client, key string) (*jira.Issue, error) {
+ req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/api/2/issue/%s", key), nil)
+ if err != nil {
+ return nil, fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ var i jira.Issue
+ if _, err = jc.Do(req, &i); err != nil {
+ return nil, fmt.Errorf("could not query JIRA: %v", err)
+ }
+ return &i, nil
+}
+
+type CreateIssueResult struct {
+ ID string `json:"id,omitempty"`
+ Key string `json:"key,omitempty"`
+}
+
+func CreateIssue(jc *jira.Client, issue *jira.Issue) (string, error) {
+ req, err := jc.NewRequest("POST", "/rest/api/2/issue", issue)
+ if err != nil {
+ return "", fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ var cir CreateIssueResult
+ if _, err = jc.Do(req, &cir); err != nil {
+ return "", fmt.Errorf("could not query JIRA: %v", err)
+ }
+ return cir.Key, nil
+}
+
+func DeleteIssue(jc *jira.Client, issue string) error {
+ req, err := jc.NewRequest("DELETE", fmt.Sprintf("/rest/api/2/issue/%s", issue), nil)
+ if err != nil {
+ return fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ if _, err = jc.Do(req, nil); err != nil {
+ return fmt.Errorf("could not query JIRA: %v", err)
+ }
+ return nil
+}
+
+type Transition struct {
+ ID string `json:"id,omitempty"`
+ Name string `json:"name,omitempty"`
+ Fields *jira.IssueFields `json:"fields,omitempty"`
+}
+
+type TransitionResult struct {
+ Transitions []Transition `json:"transitions,omitempty"`
+}
+
+func GetTransitionsForIssue(jc *jira.Client, issue string) ([]Transition, error) {
+ req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/api/2/issue/%s/transitions", issue), nil)
+ if err != nil {
+ return nil, fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ var tr TransitionResult
+ if _, err = jc.Do(req, &tr); err != nil {
+ return nil, fmt.Errorf("could no query JIRA: %v", err)
+ }
+
+ return tr.Transitions, nil
+}
+
+func TransitionIssue(jc *jira.Client, issue, transition string) error {
+ transition = strings.Replace(transition, "\n", "", -1)
+ transitions, err := GetTransitionsForIssue(jc, issue)
+ if err != nil {
+ return err
+ }
+ var id string
+ for _, t := range transitions {
+ if transition == t.Name {
+ id = t.ID
+ break
+ }
+ }
+
+ if id == "" {
+ return fmt.Errorf("no such transition")
+ }
+
+ post := map[string]interface{}{
+ "transition": map[string]interface{}{
+ "id": id,
+ },
+ }
+
+ req, err := jc.NewRequest("POST", fmt.Sprintf("/rest/api/2/issue/%s/transitions", issue), post)
+ if err != nil {
+ return fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ if _, err = jc.Do(req, nil); err != nil {
+ return fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ return nil
+}
+
+func SetFieldInIssue(jc *jira.Client, issue, field, val string) error {
+ cmd := fmt.Sprintf("/rest/api/2/issue/%s", issue)
+ method := "PUT"
+
+ var value interface{}
+ if val == "" {
+ value = nil
+ } else {
+ value = val
+ }
+
+ fields := make(map[string]interface{})
+ post := map[string]interface{}{
+ "fields": fields,
+ }
+
+ switch field {
+ case "labels":
+ var labels []string
+ if val != "" && val != "\n" {
+ labels := strings.Split(val, "\n")
+ if labels[len(labels)-1] == "" {
+ labels = labels[:len(labels)-1]
+ }
+ }
+ fields[field] = labels
+ case "issuetype", "assignee", "reporter", "creator":
+ fields[field] = map[string]interface{}{
+ "name": value,
+ }
+ default:
+ fields[field] = value
+ }
+ req, err := jc.NewRequest(method, cmd, post)
+ if err != nil {
+ return fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ if b, err := httputil.DumpRequestOut(req, true); err == nil {
+ log.Printf("SetFieldInIssue body: \n%s\n", b)
+ }
+
+ if _, err = jc.Do(req, nil); err != nil {
+ return fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ return nil
+}
+
+type CommentResult struct {
+ Comments []jira.Comment `json:"comments,omitempty"`
+}
+
+func GetCommentsForIssue(jc *jira.Client, issue string) ([]string, error) {
+ req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/api/2/issue/%s/comment?maxResults=1000", issue), nil)
+ if err != nil {
+ return nil, fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ var cr CommentResult
+ if _, err := jc.Do(req, &cr); err != nil {
+ return nil, fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ var ss []string
+ for _, c := range cr.Comments {
+ ss = append(ss, c.ID)
+ }
+
+ return ss, nil
+}
+
+func GetComment(jc *jira.Client, issue, id string) (*jira.Comment, error) {
+ req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/api/2/issue/%s/comment/%s", issue, id), nil)
+ if err != nil {
+ return nil, fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ var c jira.Comment
+ if _, err := jc.Do(req, &c); err != nil {
+ return nil, fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ return &c, nil
+}
+
+func SetComment(jc *jira.Client, issue, id, body string) error {
+ c := jira.Comment{
+ Body: body,
+ }
+
+ req, err := jc.NewRequest("PUT", fmt.Sprintf("/rest/api/2/issue/%s/comment/%s", issue, id), c)
+ if err != nil {
+ return fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ if _, err = jc.Do(req, nil); err != nil {
+ return fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ return nil
+}
+
+func AddComment(jc *jira.Client, issue, body string) error {
+ c := jira.Comment{
+ Body: body,
+ }
+
+ req, err := jc.NewRequest("POST", fmt.Sprintf("/rest/api/2/issue/%s/comment/", issue), c)
+ if err != nil {
+ return fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ if _, err = jc.Do(req, nil); err != nil {
+ return fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ return nil
+}
+
+func RemoveComment(jc *jira.Client, issue, id string) error {
+ req, err := jc.NewRequest("DELETE", fmt.Sprintf("/rest/api/2/issue/%s/comment/%s", issue, id), nil)
+ if err != nil {
+ return fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ if _, err = jc.Do(req, nil); err != nil {
+ return fmt.Errorf("could not query JIRA: %v", err)
+ }
+
+ return nil
+}
+
+type CanOpenAndLister interface {
+ CanOpen(string, qp.OpenMode) bool
+ trees.Lister
+}
+
+func OpenList(l CanOpenAndLister, user string, mode qp.OpenMode) (trees.ReadWriteAtCloser, error) {
+ if !l.CanOpen(user, mode) {
+ return nil, errors.New("permission denied")
+ }
+
+ return &trees.ListHandle{
+ Dir: l,
+ User: user,
+ }, nil
+}
+
+func StringsToStats(strs []string, Perm qp.FileMode, user, group string) []qp.Stat {
+ var stats []qp.Stat
+ for _, str := range strs {
+ stat := qp.Stat{
+ Name: str,
+ UID: user,
+ GID: group,
+ MUID: user,
+ Mode: Perm,
+ }
+ stats = append(stats, stat)
+ }
+
+ return stats
+}
+
+func StringExistsInSets(str string, sets ...[]string) bool {
+ for _, set := range sets {
+ for _, s := range set {
+ if str == s {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+type CloseSaverHandle struct {
+ onClose func() error
+ trees.ReadWriteAtCloser
+}
+
+func (csh *CloseSaverHandle) Close() error {
+ err := csh.ReadWriteAtCloser.Close()
+ if err != nil {
+ return err
+ }
+
+ if csh.onClose != nil {
+ return csh.onClose()
+ }
+
+ return nil
+}
+
+type CloseSaver struct {
+ onClose func() error
+ trees.File
+}
+
+func (cs *CloseSaver) Open(user string, mode qp.OpenMode) (trees.ReadWriteAtCloser, error) {
+ hndl, err := cs.File.Open(user, mode)
+ if err != nil {
+ return nil, err
+ }
+
+ var closer func() error
+
+ switch mode & 3 {
+ case qp.OWRITE, qp.ORDWR:
+ closer = cs.onClose
+ }
+
+ return &CloseSaverHandle{
+ ReadWriteAtCloser: hndl,
+ onClose: closer,
+ }, nil
+}
+
+func NewCloseSaver(file trees.File, onClose func() error) trees.File {
+ return &CloseSaver{
+ onClose: onClose,
+ File: file,
+ }
+}
+
+type CommandFile struct {
+ cmds map[string]func([]string) error
+ *trees.SyntheticFile
+}
+
+func (cf *CommandFile) Close() error { return nil }
+func (cf *CommandFile) ReadAt(p []byte, offset int64) (int, error) {
+ return 0, errors.New("cannot read from command file")
+}
+
+func (cf *CommandFile) WriteAt(p []byte, offset int64) (int, error) {
+ args := strings.Split(strings.Trim(string(p), " \n"), " ")
+ cmd := args[0]
+ args = args[1:]
+
+ if f, exists := cf.cmds[cmd]; exists {
+ return len(p), f(args)
+ }
+ return len(p), errors.New("no such command")
+}
+
+func (cf *CommandFile) Open(user string, mode qp.OpenMode) (trees.ReadWriteAtCloser, error) {
+ if !cf.CanOpen(user, mode) {
+ return nil, trees.ErrPermissionDenied
+ }
+
+ return cf, nil
+}
+
+func NewCommandFile(name string, perms qp.FileMode, user, group string, cmds map[string]func([]string) error) *CommandFile {
+ return &CommandFile{
+ cmds: cmds,
+ SyntheticFile: trees.NewSyntheticFile(name, perms, user, group),
+ }
+}
+
+type JiraDir struct {
+ thing interface{}
+ client *jira.Client
+ *trees.SyntheticDir
+}
+
+func (jd *JiraDir) Walk(user, name string) (trees.File, error) {
+
+ if f, ok := jd.thing.(jiraWalker); ok {
+ return f.Walk(jd.client, name)
+ }
+ if f, ok := jd.thing.(trees.Dir); ok {
+ return f.Walk(user, name)
+ }
+
+ return nil, trees.ErrPermissionDenied
+}
+
+func (jd *JiraDir) List(user string) ([]qp.Stat, error) {
+ if f, ok := jd.thing.(jiraLister); ok {
+ return f.List(jd.client)
+ }
+ if f, ok := jd.thing.(trees.Lister); ok {
+ return f.List(user)
+ }
+
+ return nil, trees.ErrPermissionDenied
+}
+
+func (jd *JiraDir) Remove(user, name string) error {
+ if f, ok := jd.thing.(jiraRemover); ok {
+ return f.Remove(jd.client, name)
+ }
+ if f, ok := jd.thing.(trees.Dir); ok {
+ return f.Remove(user, name)
+ }
+
+ return trees.ErrPermissionDenied
+}
+
+func (jd *JiraDir) Create(user, name string, perms qp.FileMode) (trees.File, error) {
+ return nil, trees.ErrPermissionDenied
+}
+
+func (jd *JiraDir) Open(user string, mode qp.OpenMode) (trees.ReadWriteAtCloser, error) {
+ if !jd.CanOpen(user, mode) {
+ return nil, errors.New("access denied")
+ }
+
+ jd.Lock()
+ defer jd.Unlock()
+ jd.Atime = time.Now()
+ jd.Opens++
+ return &trees.ListHandle{
+ Dir: jd,
+ User: user,
+ }, nil
+}
+
+func NewJiraDir(name string, perm qp.FileMode, user, group string, jc *jira.Client, thing interface{}) (*JiraDir, error) {
+ switch thing.(type) {
+ case trees.File, jiraWalker, jiraLister, jiraRemover:
+ default:
+ return nil, fmt.Errorf("unsupported type: %T", thing)
+ }
+
+ return &JiraDir{
+ thing: thing,
+ client: jc,
+ SyntheticDir: trees.NewSyntheticDir(name, perm, user, group),
+ }, nil
+}