github.com/runner-mei/ql@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  }