1
2
3
4
5
6
7
8
9
10
11
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
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
95 func (c *client) Version(_ context.Context) (*driver.Version, error) {
96 return c.version, nil
97 }
98
99
100 var validDBNameRE = regexp.MustCompile("^[a-z_][a-z0-9_$()+/-]*$")
101
102
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
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
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
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
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
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
172 func (c *client) dbPath(path string) (string, string, error) {
173
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