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