2016-05-31 13:35:35 -07:00
|
|
|
// Copyright (c) 2014 ql Authors. All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
package ql
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"path"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/cznic/mathutil"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
_ http.FileSystem = (*HTTPFS)(nil)
|
|
|
|
_ http.File = (*HTTPFile)(nil)
|
|
|
|
_ os.FileInfo = (*HTTPFile)(nil)
|
|
|
|
_ os.FileInfo = (*dirEntry)(nil)
|
|
|
|
)
|
|
|
|
|
|
|
|
type dirEntry string
|
|
|
|
|
|
|
|
func (d dirEntry) Name() string { return string(d) }
|
|
|
|
func (d dirEntry) Size() int64 { return -1 }
|
|
|
|
func (d dirEntry) Mode() os.FileMode { return os.ModeDir }
|
|
|
|
func (d dirEntry) ModTime() time.Time { return time.Time{} }
|
|
|
|
func (d dirEntry) IsDir() bool { return true }
|
|
|
|
func (d dirEntry) Sys() interface{} { return interface{}(nil) }
|
|
|
|
|
|
|
|
// A HTTPFile is returned by the HTTPFS's Open method and can be served by the
|
|
|
|
// http.FileServer implementation.
|
|
|
|
type HTTPFile struct {
|
|
|
|
closed bool
|
|
|
|
content []byte
|
|
|
|
dirEntries []os.FileInfo
|
|
|
|
isFile bool
|
|
|
|
name string
|
|
|
|
off int
|
|
|
|
}
|
|
|
|
|
|
|
|
// Close implements http.File.
|
|
|
|
func (f *HTTPFile) Close() error {
|
|
|
|
if f.closed {
|
|
|
|
return os.ErrInvalid
|
|
|
|
}
|
|
|
|
|
|
|
|
f.closed = true
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// IsDir implements os.FileInfo
|
|
|
|
func (f *HTTPFile) IsDir() bool { return !f.isFile }
|
|
|
|
|
|
|
|
// Mode implements os.FileInfo
|
|
|
|
func (f *HTTPFile) Mode() os.FileMode {
|
|
|
|
switch f.isFile {
|
|
|
|
case false:
|
|
|
|
return os.FileMode(0444)
|
|
|
|
default:
|
|
|
|
return os.ModeDir
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ModTime implements os.FileInfo
|
|
|
|
func (f *HTTPFile) ModTime() time.Time {
|
|
|
|
return time.Time{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Name implements os.FileInfo
|
|
|
|
func (f *HTTPFile) Name() string { return path.Base(f.name) }
|
|
|
|
|
|
|
|
// Size implements os.FileInfo
|
|
|
|
func (f *HTTPFile) Size() int64 {
|
|
|
|
switch f.isFile {
|
|
|
|
case false:
|
|
|
|
return -1
|
|
|
|
default:
|
|
|
|
return int64(len(f.content))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stat implements http.File.
|
|
|
|
func (f *HTTPFile) Stat() (os.FileInfo, error) { return f, nil }
|
|
|
|
|
|
|
|
// Sys implements os.FileInfo
|
|
|
|
func (f *HTTPFile) Sys() interface{} { return interface{}(nil) }
|
|
|
|
|
|
|
|
// Readdir implements http.File.
|
|
|
|
func (f *HTTPFile) Readdir(count int) ([]os.FileInfo, error) {
|
|
|
|
if f.isFile {
|
|
|
|
return nil, fmt.Errorf("not a directory: %s", f.name)
|
|
|
|
}
|
|
|
|
|
|
|
|
if count <= 0 {
|
|
|
|
r := f.dirEntries
|
|
|
|
f.dirEntries = f.dirEntries[:0]
|
|
|
|
return r, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
rq := mathutil.Min(count, len(f.dirEntries))
|
|
|
|
r := f.dirEntries[:rq]
|
|
|
|
f.dirEntries = f.dirEntries[rq:]
|
|
|
|
if len(r) != 0 {
|
|
|
|
return r, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, io.EOF
|
|
|
|
}
|
|
|
|
|
|
|
|
// Read implements http.File.
|
|
|
|
func (f *HTTPFile) Read(b []byte) (int, error) {
|
|
|
|
if f.closed {
|
|
|
|
return 0, os.ErrInvalid
|
|
|
|
}
|
|
|
|
|
|
|
|
n := copy(b, f.content[f.off:])
|
|
|
|
f.off += n
|
|
|
|
if n != 0 {
|
|
|
|
return n, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0, io.EOF
|
|
|
|
}
|
|
|
|
|
|
|
|
// Seek implements http.File.
|
|
|
|
func (f *HTTPFile) Seek(offset int64, whence int) (int64, error) {
|
|
|
|
if f.closed {
|
|
|
|
return 0, os.ErrInvalid
|
|
|
|
}
|
|
|
|
|
|
|
|
if offset < 0 {
|
|
|
|
return int64(f.off), fmt.Errorf("cannot seek before start of file")
|
|
|
|
}
|
|
|
|
|
|
|
|
switch whence {
|
|
|
|
case 0:
|
|
|
|
noff := int64(f.off) + offset
|
|
|
|
if noff > mathutil.MaxInt {
|
|
|
|
return int64(f.off), fmt.Errorf("seek target overflows int: %d", noff)
|
|
|
|
}
|
|
|
|
|
|
|
|
f.off = mathutil.Min(int(offset), len(f.content))
|
|
|
|
if f.off == int(offset) {
|
|
|
|
return offset, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return int64(f.off), io.EOF
|
|
|
|
case 1:
|
|
|
|
noff := int64(f.off) + offset
|
|
|
|
if noff > mathutil.MaxInt {
|
|
|
|
return int64(f.off), fmt.Errorf("seek target overflows int: %d", noff)
|
|
|
|
}
|
|
|
|
|
|
|
|
off := mathutil.Min(f.off+int(offset), len(f.content))
|
|
|
|
if off == f.off+int(offset) {
|
|
|
|
f.off = off
|
|
|
|
return int64(off), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
f.off = off
|
|
|
|
return int64(off), io.EOF
|
|
|
|
case 2:
|
|
|
|
noff := int64(f.off) - offset
|
|
|
|
if noff < 0 {
|
|
|
|
return int64(f.off), fmt.Errorf("cannot seek before start of file")
|
|
|
|
}
|
|
|
|
|
|
|
|
f.off = len(f.content) - int(offset)
|
|
|
|
return int64(f.off), nil
|
|
|
|
default:
|
|
|
|
return int64(f.off), fmt.Errorf("seek: invalid whence %d", whence)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// HTTPFS implements a http.FileSystem backed by data in a DB.
|
|
|
|
type HTTPFS struct {
|
|
|
|
db *DB
|
|
|
|
dir, get List
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewHTTPFS returns a http.FileSystem backed by a result record set of query.
|
|
|
|
// The record set provides two mandatory fields: path and content (the field
|
|
|
|
// names are case sensitive). Type of name must be string and type of content
|
|
|
|
// must be blob (ie. []byte). Field 'path' value is the "file" pathname, which
|
|
|
|
// must be rooted; and field 'content' value is its "data".
|
|
|
|
func (db *DB) NewHTTPFS(query string) (*HTTPFS, error) {
|
|
|
|
if _, err := Compile(query); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
dir, err := Compile(fmt.Sprintf("SELECT path FROM (%s) WHERE hasPrefix(path, $1)", query))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
get, err := Compile(fmt.Sprintf("SELECT content FROM (%s) WHERE path == $1", query))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &HTTPFS{db: db, dir: dir, get: get}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Open implements http.FileSystem. The name parameter represents a file path.
|
|
|
|
// The elements in a file path are separated by slash ('/', U+002F) characters,
|
|
|
|
// regardless of host operating system convention.
|
|
|
|
func (f *HTTPFS) Open(name string) (http.File, error) {
|
2016-09-13 12:49:35 -07:00
|
|
|
if filepath.Separator != '/' && strings.Contains(name, string(filepath.Separator)) ||
|
2016-05-31 13:35:35 -07:00
|
|
|
strings.Contains(name, "\x00") {
|
|
|
|
return nil, fmt.Errorf("invalid character in file path: %q", name)
|
|
|
|
}
|
|
|
|
|
|
|
|
name = path.Clean("/" + name)
|
|
|
|
rs, _, err := f.db.Execute(nil, f.get, name)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
n := 0
|
|
|
|
var fdata []byte
|
|
|
|
if err = rs[0].Do(false, func(data []interface{}) (more bool, err error) {
|
|
|
|
switch n {
|
|
|
|
case 0:
|
|
|
|
var ok bool
|
|
|
|
fdata, ok = data[0].([]byte)
|
|
|
|
if !ok {
|
|
|
|
return false, fmt.Errorf("open: expected blob, got %T", data[0])
|
|
|
|
}
|
|
|
|
n++
|
|
|
|
return true, nil
|
|
|
|
default:
|
|
|
|
return false, fmt.Errorf("open: more than one result was returned for %s", name)
|
|
|
|
}
|
|
|
|
}); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if n == 1 { // file found
|
|
|
|
return &HTTPFile{name: name, isFile: true, content: fdata}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
dirName := name
|
|
|
|
if dirName[len(dirName)-1] != filepath.Separator {
|
|
|
|
dirName += string(filepath.Separator)
|
|
|
|
}
|
|
|
|
// Open("/a/b"): {/a/b/c.x,/a/b/d.x,/a/e.x,/a/b/f/g.x} -> {c.x,d.x,f}
|
|
|
|
rs, _, err = f.db.Execute(nil, f.dir, dirName)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
n = 0
|
|
|
|
r := &HTTPFile{name: dirName}
|
|
|
|
m := map[string]bool{}
|
|
|
|
x := len(dirName)
|
|
|
|
if err = rs[0].Do(false, func(data []interface{}) (more bool, err error) {
|
|
|
|
n++
|
|
|
|
switch name := data[0].(type) {
|
|
|
|
case string:
|
2016-09-13 12:49:35 -07:00
|
|
|
if filepath.Separator != '/' && strings.Contains(name, string(filepath.Separator)) ||
|
2016-05-31 13:35:35 -07:00
|
|
|
strings.Contains(name, "\x00") {
|
|
|
|
return false, fmt.Errorf("invalid character in file path: %q", name)
|
|
|
|
}
|
|
|
|
|
|
|
|
name = path.Clean("/" + name)
|
|
|
|
rest := name[x:]
|
|
|
|
parts := strings.Split(rest, "/")
|
|
|
|
if len(parts) == 0 {
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
nm := parts[0]
|
|
|
|
switch len(parts) {
|
|
|
|
case 1: // file
|
|
|
|
r.dirEntries = append(r.dirEntries, &HTTPFile{isFile: true, name: nm})
|
|
|
|
default: // directory
|
|
|
|
if !m[nm] {
|
|
|
|
r.dirEntries = append(r.dirEntries, dirEntry(nm))
|
|
|
|
}
|
|
|
|
m[nm] = true
|
|
|
|
}
|
|
|
|
return true, nil
|
|
|
|
default:
|
|
|
|
return false, fmt.Errorf("expected string path, got %T(%v)", name, name)
|
|
|
|
}
|
|
|
|
}); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if n != 0 {
|
|
|
|
return r, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, os.ErrNotExist
|
|
|
|
}
|