...

Source file src/github.com/go-kivik/kivik/v4/x/fsdb/fs.go

Documentation: github.com/go-kivik/kivik/v4/x/fsdb

     1  // Licensed under the Apache License, Version 2.0 (the "License"); you may not
     2  // use this file except in compliance with the License. You may obtain a copy of
     3  // the License at
     4  //
     5  //  http://www.apache.org/licenses/LICENSE-2.0
     6  //
     7  // Unless required by applicable law or agreed to in writing, software
     8  // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     9  // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    10  // License for the specific language governing permissions and limitations under
    11  // the License.
    12  
    13  package fs
    14  
    15  import (
    16  	"context"
    17  	"encoding/json"
    18  	"errors"
    19  	"fmt"
    20  	"net/http"
    21  	"net/url"
    22  	"os"
    23  	"path/filepath"
    24  	"regexp"
    25  	"sort"
    26  	"strings"
    27  
    28  	"github.com/go-kivik/kivik/v4"
    29  	"github.com/go-kivik/kivik/v4/driver"
    30  	"github.com/go-kivik/kivik/v4/x/fsdb/cdb"
    31  	"github.com/go-kivik/kivik/v4/x/fsdb/filesystem"
    32  )
    33  
    34  const dirMode = os.FileMode(0o700)
    35  
    36  type fsDriver struct {
    37  	fs filesystem.Filesystem
    38  }
    39  
    40  var _ driver.Driver = &fsDriver{}
    41  
    42  // Identifying constants
    43  const (
    44  	Version = "0.0.1"
    45  	Vendor  = "Kivik File System Adaptor"
    46  )
    47  
    48  func init() {
    49  	kivik.Register("fs", &fsDriver{})
    50  }
    51  
    52  type client struct {
    53  	version *driver.Version
    54  	root    string
    55  	fs      filesystem.Filesystem
    56  }
    57  
    58  var _ driver.Client = &client{}
    59  
    60  func parseFileURL(dir string) (string, error) {
    61  	parsed, err := url.Parse(dir)
    62  	if parsed.Scheme != "" && parsed.Scheme != "file" {
    63  		return "", statusError{status: http.StatusBadRequest, error: fmt.Errorf("Unsupported URL scheme '%s'. Wrong driver?", parsed.Scheme)}
    64  	}
    65  	if !strings.HasPrefix(dir, "file://") {
    66  		return dir, nil
    67  	}
    68  	if err != nil {
    69  		return "", err
    70  	}
    71  	return parsed.Path, nil
    72  }
    73  
    74  func (d *fsDriver) NewClient(dir string, _ driver.Options) (driver.Client, error) {
    75  	path, err := parseFileURL(dir)
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  	fs := d.fs
    80  	if fs == nil {
    81  		fs = filesystem.Default()
    82  	}
    83  	return &client{
    84  		version: &driver.Version{
    85  			Version:     Version,
    86  			Vendor:      Vendor,
    87  			RawResponse: json.RawMessage(fmt.Sprintf(`{"version":"%s","vendor":{"name":"%s"}}`, Version, Vendor)),
    88  		},
    89  		fs:   fs,
    90  		root: path,
    91  	}, nil
    92  }
    93  
    94  // Version returns the configured server info.
    95  func (c *client) Version(_ context.Context) (*driver.Version, error) {
    96  	return c.version, nil
    97  }
    98  
    99  // Taken verbatim from http://docs.couchdb.org/en/2.0.0/api/database/common.html
   100  var validDBNameRE = regexp.MustCompile("^[a-z_][a-z0-9_$()+/-]*$")
   101  
   102  // AllDBs returns a list of all DBs present in the configured root dir.
   103  func (c *client) AllDBs(_ context.Context, options driver.Options) ([]string, error) {
   104  	opts := map[string]interface{}{}
   105  	options.Apply(opts)
   106  	if c.root == "" {
   107  		return nil, statusError{status: http.StatusBadRequest, error: errors.New("no root path provided")}
   108  	}
   109  	files, err := os.ReadDir(c.root)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  	filenames := make([]string, 0, len(files))
   114  	for _, file := range files {
   115  		dbname := cdb.UnescapeID(file.Name())
   116  		if !validDBNameRE.MatchString(dbname) {
   117  			// FIXME #64: Add option to warn about non-matching files?
   118  			continue
   119  		}
   120  		filenames = append(filenames, cdb.EscapeID(file.Name()))
   121  	}
   122  	if descending, _ := opts["descending"].(string); descending == "true" {
   123  		sort.Sort(sort.Reverse(sort.StringSlice(filenames)))
   124  	} else {
   125  		sort.Strings(filenames)
   126  	}
   127  	return filenames, nil
   128  }
   129  
   130  // CreateDB creates a database
   131  func (c *client) CreateDB(ctx context.Context, dbName string, options driver.Options) error {
   132  	exists, err := c.DBExists(ctx, dbName, options)
   133  	if err != nil {
   134  		return err
   135  	}
   136  	if exists {
   137  		return statusError{status: http.StatusPreconditionFailed, error: errors.New("database already exists")}
   138  	}
   139  	return os.Mkdir(filepath.Join(c.root, cdb.EscapeID(dbName)), dirMode)
   140  }
   141  
   142  // DBExistsreturns true if the database exists.
   143  func (c *client) DBExists(_ context.Context, dbName string, _ driver.Options) (bool, error) {
   144  	_, err := os.Stat(filepath.Join(c.root, cdb.EscapeID(dbName)))
   145  	if err == nil {
   146  		return true, nil
   147  	}
   148  	if os.IsNotExist(err) {
   149  		return false, nil
   150  	}
   151  	return false, err
   152  }
   153  
   154  // DestroyDB destroys the database
   155  func (c *client) DestroyDB(ctx context.Context, dbName string, options driver.Options) error {
   156  	exists, err := c.DBExists(ctx, dbName, options)
   157  	if err != nil {
   158  		return err
   159  	}
   160  	if !exists {
   161  		return statusError{status: http.StatusNotFound, error: errors.New("database does not exist")}
   162  	}
   163  	// FIXME #65: Be safer here about unrecognized files
   164  	return os.RemoveAll(filepath.Join(c.root, cdb.EscapeID(dbName)))
   165  }
   166  
   167  func (c *client) DB(dbName string, _ driver.Options) (driver.DB, error) {
   168  	return c.newDB(dbName)
   169  }
   170  
   171  // dbPath returns the full DB path and the dbname.
   172  func (c *client) dbPath(path string) (string, string, error) {
   173  	// As a special case, skip validation on this one
   174  	if c.root == "" && path == "." {
   175  		return ".", ".", nil
   176  	}
   177  	dbname := path
   178  	if c.root == "" {
   179  		if strings.HasPrefix(path, "file://") {
   180  			addr, err := url.Parse(path)
   181  			if err != nil {
   182  				return "", "", statusError{status: http.StatusBadRequest, error: err}
   183  			}
   184  			path = addr.Path
   185  		}
   186  		if strings.Contains(dbname, "/") {
   187  			dbname = dbname[strings.LastIndex(dbname, "/")+1:]
   188  		}
   189  	} else {
   190  		path = filepath.Join(c.root, dbname)
   191  	}
   192  	return path, dbname, nil
   193  }
   194  
   195  func (c *client) newDB(dbname string) (*db, error) {
   196  	path, name, err := c.dbPath(dbname)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	return &db{
   201  		client: c,
   202  		dbPath: path,
   203  		dbName: name,
   204  		fs:     c.fs,
   205  		cdb:    cdb.New(path, c.fs),
   206  	}, nil
   207  }
   208  

View as plain text