github.com/go-kivik/kivik/v4@v4.3.2/x/fsdb/fs.go (about) 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 }