modernc.org/ql@v1.4.7/httpfs.go (about) 1 // Copyright (c) 2014 ql Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package ql // import "modernc.org/ql" 6 7 import ( 8 "fmt" 9 "io" 10 "net/http" 11 "os" 12 "path" 13 "strings" 14 "time" 15 16 "modernc.org/mathutil" 17 ) 18 19 var ( 20 _ http.FileSystem = (*HTTPFS)(nil) 21 _ http.File = (*HTTPFile)(nil) 22 _ os.FileInfo = (*HTTPFile)(nil) 23 _ os.FileInfo = (*dirEntry)(nil) 24 ) 25 26 type dirEntry string 27 28 func (d dirEntry) Name() string { return string(d) } 29 func (d dirEntry) Size() int64 { return -1 } 30 func (d dirEntry) Mode() os.FileMode { return os.ModeDir } 31 func (d dirEntry) ModTime() time.Time { return time.Time{} } 32 func (d dirEntry) IsDir() bool { return true } 33 func (d dirEntry) Sys() interface{} { return interface{}(nil) } 34 35 // A HTTPFile is returned by the HTTPFS's Open method and can be served by the 36 // http.FileServer implementation. 37 type HTTPFile struct { 38 closed bool 39 content []byte 40 dirEntries []os.FileInfo 41 isFile bool 42 name string 43 off int 44 } 45 46 // Close implements http.File. 47 func (f *HTTPFile) Close() error { 48 if f.closed { 49 return os.ErrInvalid 50 } 51 52 f.closed = true 53 return nil 54 } 55 56 // IsDir implements os.FileInfo 57 func (f *HTTPFile) IsDir() bool { return !f.isFile } 58 59 // Mode implements os.FileInfo 60 func (f *HTTPFile) Mode() os.FileMode { 61 switch f.isFile { 62 case false: 63 return os.FileMode(0444) 64 default: 65 return os.ModeDir 66 } 67 } 68 69 // ModTime implements os.FileInfo 70 func (f *HTTPFile) ModTime() time.Time { 71 return time.Time{} 72 } 73 74 // Name implements os.FileInfo 75 func (f *HTTPFile) Name() string { return path.Base(f.name) } 76 77 // Size implements os.FileInfo 78 func (f *HTTPFile) Size() int64 { 79 switch f.isFile { 80 case false: 81 return -1 82 default: 83 return int64(len(f.content)) 84 } 85 } 86 87 // Stat implements http.File. 88 func (f *HTTPFile) Stat() (os.FileInfo, error) { return f, nil } 89 90 // Sys implements os.FileInfo 91 func (f *HTTPFile) Sys() interface{} { return interface{}(nil) } 92 93 // Readdir implements http.File. 94 func (f *HTTPFile) Readdir(count int) ([]os.FileInfo, error) { 95 if f.isFile { 96 return nil, fmt.Errorf("not a directory: %s", f.name) 97 } 98 99 if count <= 0 { 100 r := f.dirEntries 101 f.dirEntries = f.dirEntries[:0] 102 return r, nil 103 } 104 105 rq := mathutil.Min(count, len(f.dirEntries)) 106 r := f.dirEntries[:rq] 107 f.dirEntries = f.dirEntries[rq:] 108 if len(r) != 0 { 109 return r, nil 110 } 111 112 return nil, io.EOF 113 } 114 115 // Read implements http.File. 116 func (f *HTTPFile) Read(b []byte) (int, error) { 117 if f.closed { 118 return 0, os.ErrInvalid 119 } 120 121 n := copy(b, f.content[f.off:]) 122 f.off += n 123 if n != 0 { 124 return n, nil 125 } 126 127 return 0, io.EOF 128 } 129 130 // Seek implements http.File. 131 func (f *HTTPFile) Seek(offset int64, whence int) (int64, error) { 132 if f.closed { 133 return 0, os.ErrInvalid 134 } 135 136 if offset < 0 { 137 return int64(f.off), fmt.Errorf("cannot seek before start of file") 138 } 139 140 switch whence { 141 case 0: 142 noff := int64(f.off) + offset 143 if noff > mathutil.MaxInt { 144 return int64(f.off), fmt.Errorf("seek target overflows int: %d", noff) 145 } 146 147 f.off = mathutil.Min(int(offset), len(f.content)) 148 if f.off == int(offset) { 149 return offset, nil 150 } 151 152 return int64(f.off), io.EOF 153 case 1: 154 noff := int64(f.off) + offset 155 if noff > mathutil.MaxInt { 156 return int64(f.off), fmt.Errorf("seek target overflows int: %d", noff) 157 } 158 159 off := mathutil.Min(f.off+int(offset), len(f.content)) 160 if off == f.off+int(offset) { 161 f.off = off 162 return int64(off), nil 163 } 164 165 f.off = off 166 return int64(off), io.EOF 167 case 2: 168 noff := int64(f.off) - offset 169 if noff < 0 { 170 return int64(f.off), fmt.Errorf("cannot seek before start of file") 171 } 172 173 f.off = len(f.content) - int(offset) 174 return int64(f.off), nil 175 default: 176 return int64(f.off), fmt.Errorf("seek: invalid whence %d", whence) 177 } 178 } 179 180 // HTTPFS implements a http.FileSystem backed by data in a DB. 181 type HTTPFS struct { 182 db *DB 183 dir, get List 184 } 185 186 // NewHTTPFS returns a http.FileSystem backed by a result record set of query. 187 // The record set provides two mandatory fields: path and content (the field 188 // names are case sensitive). Type of name must be string and type of content 189 // must be blob (ie. []byte). Field 'path' value is the "file" pathname, which 190 // must be rooted; and field 'content' value is its "data". 191 func (db *DB) NewHTTPFS(query string) (*HTTPFS, error) { 192 if _, err := Compile(query); err != nil { 193 return nil, err 194 } 195 196 dir, err := Compile(fmt.Sprintf("SELECT path FROM (%s) WHERE hasPrefix(path, $1)", query)) 197 if err != nil { 198 return nil, err 199 } 200 201 get, err := Compile(fmt.Sprintf("SELECT content FROM (%s) WHERE path == $1", query)) 202 if err != nil { 203 return nil, err 204 } 205 206 return &HTTPFS{db: db, dir: dir, get: get}, nil 207 } 208 209 // Open implements http.FileSystem. The name parameter represents a file path. 210 // The elements in a file path are separated by slash ('/', U+002F) characters, 211 // regardless of host operating system convention. 212 func (f *HTTPFS) Open(name string) (http.File, error) { 213 name = path.Clean("/" + name) 214 rs, _, err := f.db.Execute(nil, f.get, name) 215 if err != nil { 216 return nil, err 217 } 218 219 n := 0 220 var fdata []byte 221 if err = rs[0].Do(false, func(data []interface{}) (more bool, err error) { 222 switch n { 223 case 0: 224 var ok bool 225 fdata, ok = data[0].([]byte) 226 if !ok { 227 return false, fmt.Errorf("open: expected blob, got %T", data[0]) 228 } 229 n++ 230 return true, nil 231 default: 232 return false, fmt.Errorf("open: more than one result was returned for %s", name) 233 } 234 }); err != nil { 235 return nil, err 236 } 237 238 if n == 1 { // file found 239 return &HTTPFile{name: name, isFile: true, content: fdata}, nil 240 } 241 242 dirName := name 243 if dirName[len(dirName)-1] != '/' { 244 dirName += "/" 245 } 246 // Open("/a/b"): {/a/b/c.x,/a/b/d.x,/a/e.x,/a/b/f/g.x} -> {c.x,d.x,f} 247 rs, _, err = f.db.Execute(nil, f.dir, dirName) 248 if err != nil { 249 return nil, err 250 } 251 252 n = 0 253 r := &HTTPFile{name: dirName} 254 m := map[string]bool{} 255 x := len(dirName) 256 if err = rs[0].Do(false, func(data []interface{}) (more bool, err error) { 257 n++ 258 switch name := data[0].(type) { 259 case string: 260 name = path.Clean("/" + name) 261 rest := name[x:] 262 parts := strings.Split(rest, "/") 263 if len(parts) == 0 { 264 return true, nil 265 } 266 267 nm := parts[0] 268 switch len(parts) { 269 case 1: // file 270 r.dirEntries = append(r.dirEntries, &HTTPFile{isFile: true, name: nm}) 271 default: // directory 272 if !m[nm] { 273 r.dirEntries = append(r.dirEntries, dirEntry(nm)) 274 } 275 m[nm] = true 276 } 277 return true, nil 278 default: 279 return false, fmt.Errorf("expected string path, got %T(%v)", name, name) 280 } 281 }); err != nil { 282 return nil, err 283 } 284 285 if n != 0 { 286 return r, nil 287 } 288 289 return nil, os.ErrNotExist 290 }