├── README.md ├── example.json ├── file.go └── jsonfs.go /README.md: -------------------------------------------------------------------------------- 1 | # WORK IN PROGRESS 2 | 3 | This is a demo/toy program to test the implementation and performance of 4 | the [net/styx](https://aqwari.net/net/styx) package, which itself is a 5 | work in progress. If you are having trouble getting things to work, ensure 6 | you have checked out the latest version of the `aqwari.net/net/styx` 7 | package. 8 | 9 | # BUILD 10 | 11 | go build 12 | 13 | # USE 14 | 15 | Start jsonfs on port 5640: 16 | 17 | ./jsonfs -a localhost:5640 example.json 18 | 19 | Using plan9port's `9pfuse` utility, mount the fs: 20 | 21 | 9pfuse localhost:5640 /mnt/jsonfs 22 | 23 | If you have a recent (2.6+) linux kernel, you can 24 | mount using the kernel's `v9fs` implementation. 25 | Unfortunately you'll need root access to do so 26 | without modifying `/etc/fstab`: 27 | 28 | sudo modprobe 9p 29 | sudo mount -t 9p -o \ 30 | tcp,name=`whoami`,uname=`whoami`,port=5640 \ 31 | 127.0.0.1 /mnt/jsonfs 32 | 33 | Try looking around 34 | 35 | $ ls /mnt/jsonfs 36 | apiVersion data 37 | $ cat /mnt/jsonfs/apiVersion 38 | 2.0 39 | 40 | You should see output from jsonfs, such as 41 | 42 | accepted connection from 127.0.0.1:36602 43 | → 65535 Tversion msize=8192 version="9P2000" 44 | ← 65535 Rversion msize=8192 version="9P2000" 45 | → 000 Tattach fid=1 afid=NOFID uname="droyo" aname="" 46 | ← 000 Rattach qid="type=128 ver=0 path=1" 47 | → 000 Twalk fid=1 newfid=2 "apiVersion" 48 | ← 000 Rwalk wqid="type=0 ver=0 path=2" 49 | → 000 Topen fid=2 mode=0 50 | ← 000 Ropen qid="type=0 ver=0 path=2" iounit=0 51 | → 000 Tread fid=2 offset=0 count=8168 52 | ← 000 Rread count=3 53 | → 000 Tread fid=2 offset=3 count=8168 54 | ← 000 Rread count=0 55 | 56 | When using example.json, the tree hierarchy should look 57 | something like this: 58 | 59 | $ tree /mnt/jsonfs 60 | /mnt/jsonfs 61 | ├── apiVersion 62 | └── data 63 | ├── items 64 | │   └── 0 65 | │   ├── accessControl 66 | │   │   ├── comment 67 | │   │   ├── commentVote 68 | │   │   ├── embed 69 | │   │   ├── list 70 | │   │   ├── rate 71 | │   │   ├── syndicate 72 | │   │   └── videoRespond 73 | │   ├── aspectRatio 74 | │   ├── category 75 | │   ├── commentCount 76 | │   ├── content 77 | │   │   ├── 1 78 | │   │   ├── 5 79 | │   │   └── 6 80 | │   ├── description 81 | │   ├── duration 82 | │   ├── favoriteCount 83 | │   ├── id 84 | │   ├── player 85 | │   │   └── default 86 | │   ├── rating 87 | │   ├── ratingCount 88 | │   ├── status 89 | │   │   ├── reason 90 | │   │   └── value 91 | │   ├── tags 92 | │   │   ├── 0 93 | │   │   ├── 1 94 | │   │   └── 2 95 | │   ├── thumbnail 96 | │   │   ├── default 97 | │   │   └── hqDefault 98 | │   ├── title 99 | │   ├── updated 100 | │   ├── uploaded 101 | │   ├── uploader 102 | │   └── viewCount 103 | ├── itemsPerPage 104 | ├── startIndex 105 | ├── totalItems 106 | └── updated 107 | -------------------------------------------------------------------------------- /example.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "2.0", 3 | "data": { 4 | "updated": "2010-01-07T19:58:42.949Z", 5 | "totalItems": 800, 6 | "startIndex": 1, 7 | "itemsPerPage": 1, 8 | "items": [ 9 | { 10 | "id": "hYB0mn5zh2c", 11 | "uploaded": "2007-06-05T22:07:03.000Z", 12 | "updated": "2010-01-07T13:26:50.000Z", 13 | "uploader": "GoogleDeveloperDay", 14 | "category": "News", 15 | "title": "Google Developers Day US - Maps API Introduction", 16 | "description": "Google Maps API Introduction ...", 17 | "tags": [ 18 | "GDD07", 19 | "GDD07US", 20 | "Maps" 21 | ], 22 | "thumbnail": { 23 | "default": "http://i.ytimg.com/vi/hYB0mn5zh2c/default.jpg", 24 | "hqDefault": "http://i.ytimg.com/vi/hYB0mn5zh2c/hqdefault.jpg" 25 | }, 26 | "player": { 27 | "default": "http://www.youtube.com/watch?vu003dhYB0mn5zh2c" 28 | }, 29 | "content": { 30 | "1": "rtsp://v5.cache3.c.youtube.com/CiILENy.../0/0/0/video.3gp", 31 | "5": "http://www.youtube.com/v/hYB0mn5zh2c?f...", 32 | "6": "rtsp://v1.cache1.c.youtube.com/CiILENy.../0/0/0/video.3gp" 33 | }, 34 | "duration": 2840, 35 | "aspectRatio": "widescreen", 36 | "rating": 4.63, 37 | "ratingCount": 68, 38 | "viewCount": 220101, 39 | "favoriteCount": 201, 40 | "commentCount": 22, 41 | "status": { 42 | "value": "restricted", 43 | "reason": "limitedSyndication" 44 | }, 45 | "accessControl": { 46 | "syndicate": "allowed", 47 | "commentVote": "allowed", 48 | "rate": "allowed", 49 | "list": "allowed", 50 | "comment": "allowed", 51 | "embed": "allowed", 52 | "videoRespond": "moderated" 53 | } 54 | } 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | // Turn Go types into files 14 | 15 | type fakefile struct { 16 | v interface{} 17 | offset int64 18 | set func(s string) 19 | } 20 | 21 | func (f *fakefile) ReadAt(p []byte, off int64) (int, error) { 22 | var s string 23 | if v, ok := f.v.(fmt.Stringer); ok { 24 | s = v.String() 25 | } else { 26 | s = fmt.Sprint(f.v) 27 | } 28 | if off > int64(len(s)) { 29 | return 0, io.EOF 30 | } 31 | n := copy(p, s) 32 | return n, nil 33 | } 34 | 35 | func (f *fakefile) WriteAt(p []byte, off int64) (int, error) { 36 | buf, ok := f.v.(*bytes.Buffer) 37 | if !ok { 38 | return 0, errors.New("not supported") 39 | } 40 | if off != f.offset { 41 | return 0, errors.New("no seeking") 42 | } 43 | n, err := buf.Write(p) 44 | f.offset += int64(n) 45 | return n, err 46 | } 47 | 48 | func (f *fakefile) Close() error { 49 | if f.set != nil { 50 | f.set(fmt.Sprint(f.v)) 51 | } 52 | return nil 53 | } 54 | 55 | func (f *fakefile) size() int64 { 56 | switch f.v.(type) { 57 | case map[string]interface{}, []interface{}: 58 | return 0 59 | } 60 | return int64(len(fmt.Sprint(f.v))) 61 | } 62 | 63 | type stat struct { 64 | name string 65 | file *fakefile 66 | } 67 | 68 | func (s *stat) Name() string { return s.name } 69 | func (s *stat) Sys() interface{} { return s.file } 70 | 71 | func (s *stat) ModTime() time.Time { 72 | return time.Now().Truncate(time.Hour) 73 | } 74 | 75 | func (s *stat) IsDir() bool { 76 | return s.Mode().IsDir() 77 | } 78 | 79 | func (s *stat) Mode() os.FileMode { 80 | switch s.file.v.(type) { 81 | case map[string]interface{}: 82 | return os.ModeDir | 0755 83 | case []interface{}: 84 | return os.ModeDir | 0755 85 | } 86 | return 0644 87 | } 88 | 89 | func (s *stat) Size() int64 { 90 | return s.file.size() 91 | } 92 | 93 | type dir struct { 94 | c chan stat 95 | done chan struct{} 96 | } 97 | 98 | func mkdir(val interface{}) *dir { 99 | c := make(chan stat, 10) 100 | done := make(chan struct{}) 101 | go func() { 102 | if m, ok := val.(map[string]interface{}); ok { 103 | LoopMap: 104 | for name, v := range m { 105 | select { 106 | case c <- stat{name: name, file: &fakefile{v: v}}: 107 | case <-done: 108 | break LoopMap 109 | } 110 | } 111 | } else if a, ok := val.([]interface{}); ok { 112 | LoopArray: 113 | for i, v := range a { 114 | name := strconv.Itoa(i) 115 | select { 116 | case c <- stat{name: name, file: &fakefile{v: v}}: 117 | case <-done: 118 | break LoopArray 119 | } 120 | } 121 | } 122 | close(c) 123 | }() 124 | return &dir{ 125 | c: c, 126 | done: done, 127 | } 128 | } 129 | 130 | func (d *dir) Readdir(n int) ([]os.FileInfo, error) { 131 | var err error 132 | fi := make([]os.FileInfo, 0, 10) 133 | for i := 0; i < n; i++ { 134 | s, ok := <-d.c 135 | if !ok { 136 | err = io.EOF 137 | break 138 | } 139 | fi = append(fi, &s) 140 | } 141 | return fi, err 142 | } 143 | 144 | func (d *dir) Close() error { 145 | close(d.done) 146 | return nil 147 | } 148 | -------------------------------------------------------------------------------- /jsonfs.go: -------------------------------------------------------------------------------- 1 | // Command jsonfs allows for the consumption and manipulation of a JSON 2 | // object as a file system hierarchy. 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "flag" 9 | "fmt" 10 | "log" 11 | "os" 12 | "path" 13 | "strconv" 14 | "strings" 15 | 16 | "aqwari.net/net/styx" 17 | ) 18 | 19 | var ( 20 | addr = flag.String("a", ":5640", "Port to listen on") 21 | debug = flag.Bool("D", false, "trace 9P messages") 22 | verbose = flag.Bool("v", false, "print extra info") 23 | ) 24 | 25 | type server struct { 26 | file map[string]interface{} 27 | } 28 | 29 | var logrequests styx.HandlerFunc = func(s *styx.Session) { 30 | for s.Next() { 31 | log.Printf("%q %T %s", s.User, s.Request(), s.Request().Path()) 32 | } 33 | } 34 | 35 | func main() { 36 | flag.Parse() 37 | log.SetPrefix("") 38 | log.SetFlags(0) 39 | if flag.NArg() != 1 { 40 | flag.Usage() 41 | os.Exit(2) 42 | } 43 | var srv server 44 | if f, err := os.Open(flag.Arg(0)); err != nil { 45 | log.Fatal(err) 46 | } else { 47 | d := json.NewDecoder(f) 48 | if err := d.Decode(&srv.file); err != nil { 49 | log.Fatal(err) 50 | } 51 | } 52 | var styxServer styx.Server 53 | if *verbose { 54 | styxServer.ErrorLog = log.New(os.Stderr, "", 0) 55 | } 56 | if *debug { 57 | styxServer.TraceLog = log.New(os.Stderr, "", 0) 58 | } 59 | styxServer.Addr = *addr 60 | styxServer.Handler = styx.Stack(logrequests, &srv) 61 | 62 | log.Fatal(styxServer.ListenAndServe()) 63 | } 64 | 65 | func walkTo(v interface{}, loc string) (interface{}, interface{}, bool) { 66 | cwd := v 67 | parts := strings.FieldsFunc(loc, func(r rune) bool { return r == '/' }) 68 | var parent interface{} 69 | 70 | for _, p := range parts { 71 | switch v := cwd.(type) { 72 | case map[string]interface{}: 73 | parent = v 74 | if child, ok := v[p]; !ok { 75 | return nil, nil, false 76 | } else { 77 | cwd = child 78 | } 79 | case []interface{}: 80 | parent = v 81 | i, err := strconv.Atoi(p) 82 | if err != nil { 83 | return nil, nil, false 84 | } 85 | if len(v) <= i { 86 | return nil, nil, false 87 | } 88 | cwd = v[i] 89 | default: 90 | return nil, nil, false 91 | } 92 | } 93 | return parent, cwd, true 94 | } 95 | 96 | func (srv *server) Serve9P(s *styx.Session) { 97 | for s.Next() { 98 | t := s.Request() 99 | parent, file, ok := walkTo(srv.file, t.Path()) 100 | if !ok { 101 | t.Rerror("no such file or directory") 102 | continue 103 | } 104 | fi := &stat{name: path.Base(t.Path()), file: &fakefile{v: file}} 105 | switch t := t.(type) { 106 | case styx.Twalk: 107 | t.Rwalk(fi, nil) 108 | case styx.Topen: 109 | switch v := file.(type) { 110 | case map[string]interface{}, []interface{}: 111 | t.Ropen(mkdir(v), nil) 112 | default: 113 | t.Ropen(strings.NewReader(fmt.Sprint(v)), nil) 114 | } 115 | case styx.Tstat: 116 | t.Rstat(fi, nil) 117 | case styx.Tcreate: 118 | switch v := file.(type) { 119 | case map[string]interface{}: 120 | if t.Mode.IsDir() { 121 | dir := make(map[string]interface{}) 122 | v[t.Name] = dir 123 | t.Rcreate(mkdir(dir), nil) 124 | } else { 125 | v[t.Name] = new(bytes.Buffer) 126 | t.Rcreate(&fakefile{ 127 | v: v[t.Name], 128 | set: func(s string) { v[t.Name] = s }, 129 | }, nil) 130 | } 131 | case []interface{}: 132 | i, err := strconv.Atoi(t.Name) 133 | if err != nil { 134 | t.Rerror("member of an array must be a number: %s", err) 135 | break 136 | } 137 | if t.Mode.IsDir() { 138 | dir := make(map[string]interface{}) 139 | v[i] = dir 140 | t.Rcreate(mkdir(dir), nil) 141 | } else { 142 | v[i] = new(bytes.Buffer) 143 | t.Rcreate(&fakefile{ 144 | v: v[i], 145 | set: func(s string) { v[i] = s }, 146 | }, nil) 147 | } 148 | default: 149 | t.Rerror("%s is not a directory", t.Path()) 150 | } 151 | case styx.Tremove: 152 | switch v := file.(type) { 153 | case map[string]interface{}: 154 | if len(v) > 0 { 155 | t.Rerror("directory is not empty") 156 | break 157 | } 158 | if parent != nil { 159 | if m, ok := parent.(map[string]interface{}); ok { 160 | delete(m, path.Base(t.Path())) 161 | t.Rremove(nil) 162 | } else { 163 | t.Rerror("cannot delete array element yet") 164 | break 165 | } 166 | } else { 167 | t.Rerror("permission denied") 168 | } 169 | default: 170 | if parent != nil { 171 | if m, ok := parent.(map[string]interface{}); ok { 172 | delete(m, path.Base(t.Path())) 173 | t.Rremove(nil) 174 | } else { 175 | t.Rerror("cannot delete array element") 176 | } 177 | } else { 178 | t.Rerror("permission denied") 179 | } 180 | } 181 | } 182 | } 183 | } 184 | --------------------------------------------------------------------------------