forked from sdomino/scribble
-
Notifications
You must be signed in to change notification settings - Fork 29
/
scribble.go
254 lines (199 loc) · 6.03 KB
/
scribble.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
// Package scribble is a tiny JSON database
package scribble
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
"github.com/jcelliott/lumber"
)
// Version is the current version of the project
const Version = "1.0.4"
type (
// Logger is a generic logger interface
Logger interface {
Fatal(string, ...interface{})
Error(string, ...interface{})
Warn(string, ...interface{})
Info(string, ...interface{})
Debug(string, ...interface{})
Trace(string, ...interface{})
}
// Driver is what is used to interact with the scribble database. It runs
// transactions, and provides log output
Driver struct {
mutex sync.Mutex
mutexes map[string]*sync.Mutex
dir string // the directory where scribble will create the database
log Logger // the logger scribble will log to
}
)
// Options uses for specification of working golang-scribble
type Options struct {
Logger // the logger scribble will use (configurable)
}
// New creates a new scribble database at the desired directory location, and
// returns a *Driver to then use for interacting with the database
func New(dir string, options *Options) (*Driver, error) {
//
dir = filepath.Clean(dir)
// create default options
opts := Options{}
// if options are passed in, use those
if options != nil {
opts = *options
}
// if no logger is provided, create a default
if opts.Logger == nil {
opts.Logger = lumber.NewConsoleLogger(lumber.INFO)
}
//
driver := Driver{
dir: dir,
mutexes: make(map[string]*sync.Mutex),
log: opts.Logger,
}
// if the database already exists, just use it
if _, err := os.Stat(dir); err == nil {
opts.Logger.Debug("Using '%s' (database already exists)\n", dir)
return &driver, nil
}
// if the database doesn't exist create it
opts.Logger.Debug("Creating scribble database at '%s'...\n", dir)
return &driver, os.MkdirAll(dir, 0755)
}
// Write locks the database and attempts to write the record to the database under
// the [collection] specified with the [resource] name given
func (d *Driver) Write(collection, resource string, v interface{}) error {
// ensure there is a place to save record
if collection == "" {
return fmt.Errorf("Missing collection - no place to save record!")
}
// ensure there is a resource (name) to save record as
if resource == "" {
return fmt.Errorf("Missing resource - unable to save record (no name)!")
}
mutex := d.getOrCreateMutex(collection)
mutex.Lock()
defer mutex.Unlock()
//
dir := filepath.Join(d.dir, collection)
fnlPath := filepath.Join(dir, resource+".json")
tmpPath := fnlPath + ".tmp"
// create collection directory
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
//
b, err := json.MarshalIndent(v, "", "\t")
if err != nil {
return err
}
// write marshaled data to the temp file
if err := ioutil.WriteFile(tmpPath, b, 0644); err != nil {
return err
}
// move final file into place
return os.Rename(tmpPath, fnlPath)
}
// Read a record from the database
func (d *Driver) Read(collection, resource string, v interface{}) error {
// ensure there is a place to save record
if collection == "" {
return fmt.Errorf("Missing collection - no place to save record!")
}
// ensure there is a resource (name) to save record as
if resource == "" {
return fmt.Errorf("Missing resource - unable to save record (no name)!")
}
//
record := filepath.Join(d.dir, collection, resource)
// check to see if file exists
if _, err := stat(record); err != nil {
return err
}
// read record from database
b, err := ioutil.ReadFile(record + ".json")
if err != nil {
return err
}
// unmarshal data
return json.Unmarshal(b, &v)
}
// ReadAll records from a collection; this is returned as a slice of strings because
// there is no way of knowing what type the record is.
func (d *Driver) ReadAll(collection string) ([]string, error) {
// ensure there is a collection to read
if collection == "" {
return nil, fmt.Errorf("Missing collection - unable to record location!")
}
//
dir := filepath.Join(d.dir, collection)
// check to see if collection (directory) exists
if _, err := stat(dir); err != nil {
return nil, err
}
// read all the files in the transaction.Collection; an error here just means
// the collection is either empty or doesn't exist
files, _ := ioutil.ReadDir(dir)
// the files read from the database
var records []string
// iterate over each of the files, attempting to read the file. If successful
// append the files to the collection of read files
for _, file := range files {
b, err := ioutil.ReadFile(filepath.Join(dir, file.Name()))
if err != nil {
return nil, err
}
// append read file
records = append(records, string(b))
}
// unmarhsal the read files as a comma delimeted byte array
return records, nil
}
// Delete locks that database and then attempts to remove the collection/resource
// specified by [path]
func (d *Driver) Delete(collection, resource string) error {
path := filepath.Join(collection, resource)
//
mutex := d.getOrCreateMutex(collection)
mutex.Lock()
defer mutex.Unlock()
//
dir := filepath.Join(d.dir, path)
switch fi, err := stat(dir); {
// if fi is nil or error is not nil return
case fi == nil, err != nil:
return fmt.Errorf("Unable to find file or directory named %v\n", path)
// remove directory and all contents
case fi.Mode().IsDir():
return os.RemoveAll(dir)
// remove file
case fi.Mode().IsRegular():
return os.RemoveAll(dir + ".json")
}
return nil
}
//
func stat(path string) (fi os.FileInfo, err error) {
// check for dir, if path isn't a directory check to see if it's a file
if fi, err = os.Stat(path); os.IsNotExist(err) {
fi, err = os.Stat(path + ".json")
}
return
}
// getOrCreateMutex creates a new collection specific mutex any time a collection
// is being modfied to avoid unsafe operations
func (d *Driver) getOrCreateMutex(collection string) *sync.Mutex {
d.mutex.Lock()
defer d.mutex.Unlock()
m, ok := d.mutexes[collection]
// if the mutex doesn't exist make it
if !ok {
m = &sync.Mutex{}
d.mutexes[collection] = m
}
return m
}