diff --git a/main.go b/main.go index 12297f2..96dca26 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ type Server interface { func main() { var challenge int - flag.IntVar(&challenge, "challenge",9, "Challenge number") + flag.IntVar(&challenge, "challenge",10, "Challenge number") flag.Parse() var port uint16 @@ -39,6 +39,8 @@ func main() { server = NewSecureServer(port); case 9: server = NewJobServer(port); + case 10: + server = NewVcsServer(port); default: fmt.Printf("Unknown challenge\n") os.Exit(1) diff --git a/spec/vcs.md b/spec/vcs.md new file mode 100644 index 0000000..153a882 --- /dev/null +++ b/spec/vcs.md @@ -0,0 +1,133 @@ +# Discovery +``` +$ nc vcs.protohackers.com 30307 +READY +help +OK usage: HELP|GET|PUT|LIST +READY +list / +OK 0 +READY +put /snack 4 +bla +OK r1 +READY +list / +OK 1 +snack r1 +READY +get /snack +OK 4 +bla +READY +put /snack 3 +al +OK r2 +READY +READY +list / +OK 1 +snack r2 +READY +put /snik/snak +ERR usage: PUT file length newline data +READY +put /snik/snak 3 +ji +OK r1 +READY +list / +OK 2 +snack r2 +snik/ DIR +READY +get +ERR usage: GET file [revision] +READY +get /snack r1 +OK 4 +bla +READY +``` + +# overview +this system is a simple filesystem that maintains file versions. All paths are absolute, there is no current working directory. + +This is a tcp protocol that strictly prescribes who talks when. it has two modes: + +* in command mode, server prints READY, then reads until it finds a newline +* in data mode, server reads a prespecified amount of bytes +* on connection, the server goes into command mode +* 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 + +The filesystem must be global, shared by all users + +# commands +commands and arguments are separated by spaces. + +## help +OK response describing all commands. + +if arguments are given they are ignored + +## LIST +argument: dir + +if more or less arguments are provided, response is 'ERR usage: LIST dir' + +dir must start with a / and can contain more than one / (subdirs). trailing / is optional. Two consecutive slashed are not allowed. + +if dir is an illegal dir name (does not start with a /), response is 'ERR illegal dir name' + +If dir does not exist or is empty, the response is 'OK 0' + +the response followed by the number of lines in the list + +each line starts with the name of the item + +if the item is a directory, the name is postfixed with a / and followed by the word DIR + +If the item is a file, the name is followed by the revision, prefixed by 'r' + +## PUT +arguments: file length + +if more or less arguments are provided, the response is 'ERR usage: PUT file length newline data' + +filenames have the same rules as dirnames but can not end with a / + +if an illegal filename is given, the response is 'ERR illegal file name' + +if length is not a number or a negative number, it is interpreted as 0 + +if no errors are found, the server goes into data mode to read the specified amount of bytes (can be 0) + +the bytes are then stored in the file. if the file is new, it is given revision 1, otherwise the revision is incremented. + +the response is 'OK r1' with r1 being the latest revision of the file. + +## GET +arguments: file [revision] + +If the wrong number of args is given, the response is 'ERR usage: GET file [revision]' + +the file argument follows the same rules as PUT + +if the file is not found, the response is 'ERR no such file' + +if the file is a directory, the response is the same as when the file is not found + +the revision is a number, optionally prefixed by a "r" + +if the revision is postfixed by non-numeric chars, these are ignored + +if the revision is not a number, it is not found + +if the revision is not found, the response is 'ERR no such revision' + +if no revision is given, the latest one is used. + +The response is 'OK 5' where 5 is the number of bytes, followed by that number of bytes. + diff --git a/vcs.go b/vcs.go new file mode 100644 index 0000000..71792e3 --- /dev/null +++ b/vcs.go @@ -0,0 +1,86 @@ +package main + +import ( +"net" +"os" +"fmt" +"bufio" +) + +type PutRequest struct { + file string + content string + callback chan bool +} + +type VcsServer struct { + port uint16 + q chan PutRequest +} + +func NewVcsServer(port uint16) *VcsServer { + return &VcsServer{ + port, + make(chan PutRequest), + } +} + +type VcsSession struct{ + server *VcsServer + con net.Conn +} + +func NewVcsSession(server *VcsServer, con net.Conn) *VcsSession { + return &VcsSession{ + server, + con, + } +} + + + +func (s *VcsServer) central(){ + for m := range s.q { + fmt.Printf("I %+v\n", m) + } +} + + +func (s *VcsServer) Run() { + go s.central() + addr := fmt.Sprintf("0.0.0.0:%d", s.port) + server, err := net.Listen("tcp", addr) + if err != nil { + fmt.Println("Error listening:", err.Error()) + os.Exit(1) + } + defer server.Close() + fmt.Println("VcsServer waiting for client...") + for { + connection, err := server.Accept() + if err != nil { + fmt.Println("Error accepting: ", err.Error()) + os.Exit(1) + } + fmt.Println("client connected") + s.processClient(connection) + } + close(s.q) +} + +func (s *VcsServer) processClient(con net.Conn) { + session := NewVcsSession(s, con) + go session.vcsHandler() +} + + +func (s *VcsSession) vcsHandler() { + r := bufio.NewReaderSize(s.con, 102400) + msg, err := r.ReadString('\n') + + for err == nil { + fmt.Print(msg) + msg, err = r.ReadString('\n') + } +} +