diff --git a/spec/vcs.md b/spec/vcs.md index 153a882..b9e317c 100644 --- a/spec/vcs.md +++ b/spec/vcs.md @@ -61,14 +61,19 @@ This is a tcp protocol that strictly prescribes who talks when. it has two modes * after a newline, if the command is put and it is a valid command, the server goes into data mode. after the specified amount of bytes has been read, the server prints a response (see below) and returns to command mode * for all other commands, the server prints a response and returns to command mode * responses start with OK or ERR and can be multiple lines +* if a non existing command is given, the response is 'ERR illegal method: [cmd]' The filesystem must be global, shared by all users +Filenames are case sensitive + +A node is listed as a file if it has 1 or more revisions. otherwise, it is listed as a dir. it can be both at the same time # commands commands and arguments are separated by spaces. ## help OK response describing all commands. +'OK usage: HELP|GET|PUT|LIST' if arguments are given they are ignored diff --git a/vcs.go b/vcs.go index 71792e3..5f518ab 100644 --- a/vcs.go +++ b/vcs.go @@ -5,49 +5,133 @@ import ( "os" "fmt" "bufio" +"strings" +"strconv" +"io" ) +type VcsNode struct { + children map[string]*VcsNode + revisions []string +} + type PutRequest struct { file string content string - callback chan bool + callback chan PutResult +} + +type PutResult struct { + rev uint + err error +} + +type VcsStorage struct { + q chan *PutRequest + root *VcsNode } type VcsServer struct { port uint16 - q chan PutRequest + storage VcsStorage +} +func NewPutRequest(file string, content string) *PutRequest { + return &PutRequest{ + file, + content, + make(chan PutResult), + } } +func NewVcsNode() *VcsNode { + return &VcsNode { + make(map[string]*VcsNode), + nil, + } +} func NewVcsServer(port uint16) *VcsServer { return &VcsServer{ port, - make(chan PutRequest), + VcsStorage{ + make(chan *PutRequest), + NewVcsNode(), + }, } } type VcsSession struct{ - server *VcsServer + storage *VcsStorage con net.Conn } -func NewVcsSession(server *VcsServer, con net.Conn) *VcsSession { +func NewVcsSession(storage *VcsStorage, con net.Conn) *VcsSession { return &VcsSession{ - server, + storage, con, } } +func (n *VcsNode) get(path []string, create bool) (*VcsNode, error) { + if len(path) == 0 { return n, nil } + part := path[0] + nx, ok := n.children[part] + if ! ok { + if create { + nx = NewVcsNode() + n.children[part] = nx + }else{ + return nil, fmt.Errorf("no such file") + } + } + return nx.get(path[1:], create) +} +func parsePath(strPath string, isDir bool) ([]string, error) { + p := strings.Split(strPath, "/") + fmt.Println(p) + itype := "file" + if isDir { + itype = "dir" + } + if p[0] != "" { + return []string{}, fmt.Errorf("invalid %s name", itype) + } + if p[len(p)-1] == "" { + if !isDir { + return []string{}, fmt.Errorf("invalid file name") + }else{ + p = p[:len(p)-2] + } + } + p = p[1:] + return p, nil +} -func (s *VcsServer) central(){ +func (s *VcsStorage) hdlPut(m *PutRequest) (uint, error) { + path, err := parsePath(m.file, false ) + if err != nil { + return 0, err + } + f, err := s.root.get(path, true) + if err != nil { + return 0, err + } + f.revisions = append(f.revisions, m.content) + return uint(len(f.revisions)), nil + +} + +func (s *VcsStorage) central(){ for m := range s.q { - fmt.Printf("I %+v\n", m) + rev, err := s.hdlPut(m) + m.callback <- PutResult{rev, err} } } + func (s *VcsServer) Run() { - go s.central() + go s.storage.central() addr := fmt.Sprintf("0.0.0.0:%d", s.port) server, err := net.Listen("tcp", addr) if err != nil { @@ -65,22 +149,72 @@ func (s *VcsServer) Run() { fmt.Println("client connected") s.processClient(connection) } - close(s.q) } func (s *VcsServer) processClient(con net.Conn) { - session := NewVcsSession(s, con) + session := NewVcsSession(&s.storage, con) go session.vcsHandler() } +func (s *VcsSession) hdlHelp() error { + s.write("OK usage: HELP|GET|PUT|LIST\n") + return nil +} + + + +func (s *VcsSession) hdlPut(args []string, r *bufio.Reader) error { + file := args[0] + clen, err := strconv.Atoi(args[1]) + if err != nil { clen = 0 } + buf := make([]byte, clen) + _, err = io.ReadFull(r, buf) + if err!= nil { return err } + rq := NewPutRequest(file, string(buf)) + s.storage.q <- rq + result := <- rq.callback + err = result.err + if err == nil { + s.write(fmt.Sprintf("OK r%d\n", result.rev)) + } + return err +} + +func (s *VcsSession) process(cmd string, r *bufio.Reader) error { + fmt.Println(cmd) + var err error + parts := strings.Split(cmd, " ") + if len(parts) == 0 { return fmt.Errorf("illegal method:")} + method := strings.ToLower(parts[0]) + switch method { + case "help": err = s.hdlHelp() + case "put": err = s.hdlPut(parts[1:], r) + default: err = fmt.Errorf("illegal method: %s", method) + } + return err +} + +func (s *VcsSession) write(m string){ + s.con.Write([]byte(m)) +} + +func (s *VcsSession) commandMode(r *bufio.Reader) error { + s.write("READY\n") + msg, err := r.ReadString('\n') + if err != nil { return err } + msg = strings.TrimSpace(msg) + perr := s.process(msg, r) + if perr != nil { + s.write(fmt.Sprintf("ERR %s\n", perr.Error())) + } + return nil +} func (s *VcsSession) vcsHandler() { r := bufio.NewReaderSize(s.con, 102400) - msg, err := r.ReadString('\n') - + var err error for err == nil { - fmt.Print(msg) - msg, err = r.ReadString('\n') + err = s.commandMode(r) } }