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  }