├── .gitignore
├── .travis.yml
├── LICENSE
├── Makefile
├── README.md
├── all_test.go
├── cmd
└── gitdb
│ ├── gitdb.go
│ ├── gitdb_test.go
│ └── template.go
├── config.go
├── db.go
├── db_mock.go
├── db_mock_test.go
├── db_test.go
├── driver.go
├── driver_git.go
├── driver_gitbinary.go
├── driver_local.go
├── errors.go
├── event.go
├── example
├── booking
│ ├── collection.go
│ ├── enums.go
│ └── model.go
└── main.go
├── go.mod
├── go.sum
├── index.go
├── init.go
├── init_test.go
├── internal
├── crypto
│ └── security.go
├── db
│ ├── block.go
│ ├── dataset.go
│ └── record.go
├── digital
│ └── size.go
└── errors
│ └── errors.go
├── lock.go
├── log.go
├── mail.go
├── mail_test.go
├── model.go
├── paths.go
├── read.go
├── read_test.go
├── schema.go
├── schema_test.go
├── ssh.go
├── static
├── css
│ └── app.css
├── errors.html
├── index.html
├── js
│ └── app.js
├── list.html
├── sidebar.html
└── view.html
├── sync.go
├── sync_test.go
├── testdata
├── v1
│ └── data
│ │ └── data
│ │ ├── Message
│ │ └── b0.json
│ │ └── MessageV2
│ │ └── 202003.json
└── v2
│ └── data
│ └── data
│ ├── Message
│ └── b0.json
│ └── MessageV2
│ └── 202003.json
├── tools
├── fix_corrupt_head.sh
└── fix_corrupt_missing_objects.sh
├── transaction.go
├── transaction_test.go
├── ui.go
├── ui_static.go
├── ui_test.go
├── ui_view.go
├── ui_viewmodels.go
├── upload.go
├── upload_test.go
├── user.go
├── user_test.go
├── write.go
└── write_test.go
/.gitignore:
--------------------------------------------------------------------------------
1 | example/data
2 | example/example
3 | example/ui_gitdb.go
4 | .DS_Store
5 | .vscode
6 | .idea
7 | vendor
8 | *.test
9 | .gitdb
10 | cover.out
11 | prof.cpu
12 | prof.mem
13 | bin
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | go_import_path: github.com/gogitdb/github
3 |
4 | sudo: false
5 | go:
6 | - 1.14
7 | branches:
8 | only:
9 | - master
10 | script:
11 | - make test
12 | after_success:
13 | - bash <(curl -s https://codecov.io/bash)
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Okechukwu Ugwu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: test testdel example install release
2 | testdel:
3 | go test ./... -coverprofile=cover.out -v
4 | go tool cover -html=cover.out
5 | rm -f cover.out
6 | test:
7 | go test ./... -coverprofile=cover.out
8 | go tool cover -func=cover.out
9 | example:
10 | cd example && rm -Rf data && go run *.go && cd -
11 | install:
12 | go install github.com/gogitdb/gitdb/v2/cmd/gitdb
13 | release:
14 | go install github.com/gogitdb/gitdb/v2/cmd/gitdb
15 | go generate
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | GitDB
2 | =====
3 |
4 | [](https://goreportcard.com/report/github.com/gogitdb/gitdb)
5 | [](https://codecov.io/gh/gogitdb/gitdb)
6 | [](https://travis-ci.com/gogitdb/gitdb)
7 | [](https://godoc.org/github.com/gogitdb/gitdb)
8 | [](https://github.com/gogitdb/gitdb/releases)
9 | [](https://github.com/gogitdb/gitdb/blob/master/LICENSE)
10 |
11 | ## What is GitDB?
12 |
13 | > GitDB is not a binary. It’s a library!
14 |
15 | GitDB is a decentralized document database written in Go that uses [Git](https://git-scm.com/) under the hood to provide database-like functionalities via strictly defined interfaces.
16 |
17 | GitDB allows developers to create Models of objects in their application which implement a Model Interface that can access it's persistence features. This allows GitDB to work with these objects in database operations.
18 |
19 | ## Why GitDB - motivation behind project?
20 |
21 | - A need for a database that was quick and simple to set up
22 | - A need for a database that was decentralized and each participating client in a system can store their data independent of other clients.
23 |
24 |
25 | ## Features
26 |
27 | - Decentralized
28 | - Document store
29 | - Embedded into your go application
30 | - Encryption (encrypt on write, decrypt on read)
31 | - Record locking.
32 | - Simple Indexing System
33 | - Transactions
34 | - Web UI
35 |
36 |
37 | ## Project versioning
38 |
39 | GitDB uses [semantic versioning](http://semver.org).
40 | API should not change between patch and minor releases.
41 | New minor versions may add additional features to the API.
42 |
43 | ## Table of Contents
44 |
45 | - [Getting Started](#getting-started)
46 | - [Installing](#installing)
47 | - [Configuration](#configuration)
48 | - [Opening a database](#opening-a-database)
49 | - [Inserting/Updating a record](#insertingupdating-a-record)
50 | - [Fetching a single record](#fetching-a-single-record)
51 | - [Fetching all records in a dataset](#fetching-all-records-in-a-dataset)
52 | - [Deleting a record](#deleting-a-record)
53 | - [Search for records](#search-for-records)
54 | - [Transactions](#transactions)
55 | - [Encryption](#encryption)
56 | - [Resources](#resources)
57 | - [Caveats & Limitations](#caveats--limitations)
58 | - [Reading the Source](#reading-the-source)
59 |
60 |
61 | ## Getting Started
62 |
63 | ### Installing
64 |
65 | To start using GitDB, install Go and run `go get`:
66 |
67 | ```sh
68 | $ go get github.com/gogitdb/gitdb/v2
69 | ```
70 |
71 |
72 | ### Configuration
73 |
74 | Below are configuration options provided by GitDB
75 |
76 |
77 | Name
78 | Description
79 | Type
80 | Required
81 | Default
82 |
83 |
84 | DbPath
85 | Path on your machine where you want GitDB to create/clone your database
86 | string
87 | Y
88 | N/A
89 |
90 |
91 | ConnectionName
92 | Unique name for gitdb connection. Use this when opening multiple GitDB connections
93 | string
94 | N
95 | "default"
96 |
97 |
98 | OnlineRemote
99 | URL for remote git server you want GitDB to sync with e.g git@github.com:user/db.git or https://github.com/user/db.git.
100 | Note: The first time GitDB runs, it will automatically generate ssh keys and will automatically attempt to use this key to sync with the OnlineRemote,
101 | therefore ensure that the generated keys are added to this git server. The ssh keys can be found at Config.DbPath/.gitdb/ssh
102 |
103 | string
104 | N
105 | ""
106 |
107 |
108 | SyncInterval
109 | This controls how often you want GitDB to sync with the online remote
110 | time.Duration.
111 | N
112 | 5s
113 |
114 |
115 | EncryptionKey
116 | 16,24 or 32 byte string used to provide AES encryption for Models that implement ShouldEncrypt
117 | string
118 | N
119 | ""
120 |
121 |
122 | User
123 | This specifies the user connected to the Gitdb and will be used to commit all changes to the database
124 | gitdb.User
125 | N
126 | ghost <ghost@gitdb.local>
127 |
128 |
129 | EnableUI
130 | Use this option to enable GitDB web user interface
131 | bool
132 | N
133 | false
134 |
135 |
136 | UIPort
137 | Use this option to change the default port which GitDB uses to serve it's web user interface
138 | int
139 | N
140 | 4120
141 |
142 |
143 | Factory
144 | For backward compatibity with v1. In v1 GitDB needed a factory method to be able construct concrete Model for certain database operations.
145 | This has now been dropped in v2
146 |
147 | func(dataset string) gitdb.Model
148 | N
149 | nil
150 |
151 |
152 | Mock
153 | Flag used for testing apps. If true, will return a mock GitDB connection
154 | bool
155 | N
156 | false
157 |
158 |
159 |
160 | You can configure GitDB either using the constructor or constructing it yourself
161 |
162 | ```go
163 | cfg := gitdb.NewConfig(path)
164 | //or
165 | cfg := gitdb.Config{
166 | DbPath: path
167 | }
168 | ```
169 |
170 |
171 |
173 |
174 | ### Importing GitDB
175 |
176 | To use GitDB as an embedded document store, import as:
177 |
178 | ```go
179 | import "github.com/gogitdb/gitdb/v2"
180 |
181 | cfg := gitdb.NewConfig(path)
182 | db, err := gitdb.Open(cfg)
183 | if err != nil {
184 | log.Fatal(err)
185 | }
186 | defer db.Close()
187 | ```
188 |
189 |
190 | ### Opening a database
191 | ```go
192 | package main
193 |
194 | import (
195 | "log"
196 | "github.com/gogitdb/gitdb/v2"
197 | )
198 |
199 | func main() {
200 |
201 | cfg := gitdb.NewConfig("/tmp/data")
202 | // Open will create or clone down a git repo
203 | // in configured path if it does not exist.
204 | db, err := gitdb.Open(cfg)
205 | if err != nil {
206 | log.Fatal(err)
207 | }
208 | defer db.Close()
209 |
210 | ...
211 | }
212 | ```
213 |
214 | ### Models
215 |
216 | A Model is a struct that represents a record in GitDB. GitDB only works with models that implement the gidb.Model interface
217 |
218 | gitdb.TimeStampedModel is a simple struct that allows you to easily add CreatedAt and UpdatedAt to all the Models in your application and will automatically time stamp them before persisting to GitDB. You can write your own base Models to embed common fields across your application Models
219 |
220 | ```go
221 | type BankAccount struct {
222 | //TimeStampedModel will add CreatedAt and UpdatedAt fields this Model
223 | gitdb.TimeStampedModel
224 | AccountType string
225 | AccountNo string
226 | Currency string
227 | Name string
228 | }
229 |
230 | func (b *BankAccount) GetSchema() *gitdb.Schema {
231 | //Dataset Name
232 | name := "Accounts"
233 | //Block ID
234 | block := b.CreatedAt.Format("200601")
235 | //Record ID
236 | record := b.AccountNo
237 |
238 | //Indexes speed up searching
239 | indexes := make(map[string]interface{})
240 | indexes["AccountType"] = b.AccountType
241 |
242 | return gitdb.NewSchema(name, block, record, indexes)
243 | }
244 |
245 | func (b *BankAccount) Validate() error { return nil }
246 | func (b *BankAccount) IsLockable() bool { return false }
247 | func (b *BankAccount) ShouldEncrypt() bool { return false }
248 | func (b *BankAccount) GetLockFileNames() []string { return []string{} }
249 |
250 | ...
251 |
252 | ```
253 |
254 | ### Inserting/Updating a record
255 | ```go
256 | package main
257 |
258 | import (
259 | "log"
260 | "github.com/gogitdb/gitdb/v2"
261 | )
262 |
263 | func main(){
264 | cfg := gitdb.NewConfig("/tmp/data")
265 | db, err := gitdb.Open(cfg)
266 | if err != nil {
267 | log.Fatal(err)
268 | }
269 | defer db.Close()
270 |
271 | //populate model
272 | account := &BankAccount{}
273 | account.AccountNo = "0123456789"
274 | account.AccountType = "Savings"
275 | account.Currency = "GBP"
276 | account.Name = "Foo Bar"
277 |
278 | err = db.Insert(account)
279 | if err != nil {
280 | log.Println(err)
281 | }
282 |
283 | //get account id
284 | log.Println(gitdb.Id(account))
285 |
286 | //update account name
287 | account.Name = "Bar Foo"
288 | err = db.Insert(account)
289 | if err != nil {
290 | log.Println(err)
291 | }
292 | }
293 | ```
294 |
295 | ### Fetching a single record
296 | ```go
297 | package main
298 | import (
299 | "log"
300 | "github.com/gogitdb/gitdb/v2"
301 | )
302 |
303 | func main(){
304 | cfg := gitdb.NewConfig("/tmp/data")
305 | db, err := gitdb.Open(cfg)
306 | if err != nil {
307 | log.Fatal(err)
308 | }
309 | defer db.Close()
310 |
311 | //model to passed to Get to store result
312 | var account BankAccount
313 | if err := db.Get("Accounts/202003/0123456789", &account); err != nil {
314 | log.Println(err)
315 | }
316 | }
317 | ```
318 | ### Fetching all records in a dataset
319 | ```go
320 | package main
321 |
322 | import (
323 | "fmt"
324 | "log"
325 | "github.com/gogitdb/gitdb/v2"
326 | )
327 |
328 | func main(){
329 | cfg := gitdb.NewConfig("/tmp/data")
330 | db, err := gitdb.Open(cfg)
331 | if err != nil {
332 | log.Fatal(err)
333 | }
334 | defer db.Close()
335 |
336 | records, err := db.Fetch("Accounts")
337 | if err != nil {
338 | log.Print(err)
339 | return
340 | }
341 |
342 | accounts := []*BankAccount{}
343 | for _, r := range records {
344 | b := &BankAccount{}
345 | r.Hydrate(b)
346 | accounts = append(accounts, b)
347 | log.Print(fmt.Sprintf("%s-%s", gitdb.ID(b), b.AccountNo))
348 | }
349 | }
350 |
351 | ```
352 |
353 | ### Fetching all records from specific block in a dataset
354 | ```go
355 | package main
356 |
357 | import (
358 | "fmt"
359 | "log"
360 | "github.com/gogitdb/gitdb/v2"
361 | )
362 |
363 | func main(){
364 | cfg := gitdb.NewConfig("/tmp/data")
365 | db, err := gitdb.Open(cfg)
366 | if err != nil {
367 | log.Fatal(err)
368 | }
369 | defer db.Close()
370 |
371 | records, err := db.Fetch("Accounts", "b0", "b1")
372 | if err != nil {
373 | log.Print(err)
374 | return
375 | }
376 |
377 | var accounts []*BankAccount
378 | for _, r := range records {
379 | b := &BankAccount{}
380 | r.Hydrate(b)
381 | accounts = append(accounts, b)
382 | log.Print(fmt.Sprintf("%s-%s", gitdb.ID(b), b.AccountNo))
383 | }
384 | }
385 |
386 | ```
387 |
388 | ### Deleting a record
389 | ```go
390 | package main
391 |
392 | import (
393 | "log"
394 | "github.com/gogitdb/gitdb/v2"
395 | )
396 |
397 | func main(){
398 | cfg := gitdb.NewConfig("/tmp/data")
399 | db, err := gitdb.Open(cfg)
400 | if err != nil {
401 | log.Fatal(err)
402 | }
403 | defer db.Close()
404 |
405 | if err := db.Delete("Accounts/202003/0123456789"); err != nil {
406 | log.Print(err)
407 | }
408 | }
409 | ```
410 |
411 | ### Search for records
412 | ```go
413 | package main
414 |
415 | import (
416 | "fmt"
417 | "log"
418 | "github.com/gogitdb/gitdb/v2"
419 | )
420 |
421 | func main(){
422 | cfg := gitdb.NewConfig("/tmp/data")
423 | db, err := gitdb.Open(cfg)
424 | if err != nil {
425 | log.Fatal(err)
426 | }
427 | defer db.Close()
428 |
429 | //Find all records that have savings account type
430 | searchParam := &db.SearchParam{Index: "AccountType", Value: "Savings"}
431 | records, err := dbconn.Search("Accounts", []*db.SearchParam{searchParam}, gitdb.SearchEquals)
432 | if err != nil {
433 | log.Println(err.Error())
434 | return
435 | }
436 |
437 | accounts := []*BankAccount{}
438 | for _, r := range records {
439 | b := &BankAccount{}
440 | r.Hydrate(b)
441 | accounts = append(accounts, b)
442 | log.Print(fmt.Sprintf("%s-%s", b.ID, b.CreatedAt))
443 | }
444 | }
445 | ```
446 |
447 | ### Transactions
448 | ```go
449 | package main
450 |
451 | import (
452 | "log"
453 | "github.com/gogitdb/gitdb/v2"
454 | )
455 |
456 | func main() {
457 | cfg := gitdb.NewConfig("/tmp/data")
458 | db, err := gitdb.Open(cfg)
459 | if err != nil {
460 | log.Fatal(err)
461 | }
462 | defer db.Close()
463 |
464 | func accountUpgradeFuncOne() error { println("accountUpgradeFuncOne..."); return nil }
465 | func accountUpgradeFuncTwo() error { println("accountUpgradeFuncTwo..."); return errors.New("accountUpgradeFuncTwo failed") }
466 | func accountUpgradeFuncThree() error { println("accountUpgradeFuncThree"); return nil }
467 |
468 | tx := db.StartTransaction("AccountUpgrade")
469 | tx.AddOperation(accountUpgradeFuncOne)
470 | tx.AddOperation(accountUpgradeFuncTwo)
471 | tx.AddOperation(accountUpgradeFuncThree)
472 | terr := tx.Commit()
473 | if terr != nil {
474 | log.Print(terr)
475 | }
476 | }
477 | ```
478 |
479 | ### Encryption
480 |
481 | GitDB suppports AES encryption and is done on a Model level, which means you can have a database with different Models where some are encrypted and others are not. To encrypt your data, your Model must implement `ShouldEncrypt()` to return true and you must set `gitdb.Config.EncryptionKey`. For maximum security set this key to a 32 byte string to select AES-256
482 |
483 | ```go
484 | package main
485 |
486 | import (
487 | "log"
488 | "github.com/gogitdb/gitdb/v2"
489 | )
490 |
491 | func main(){
492 | cfg := gitdb.NewConfig("/tmp/data")
493 | cfg.EncryptionKey = "a_32_bytes_string_for_AES-256"
494 | db, err := gitdb.Open(cfg)
495 | if err != nil {
496 | log.Fatal(err)
497 | }
498 | defer db.Close()
499 |
500 | //populate model
501 | account := &BankAccount{}
502 | account.AccountNo = "0123456789"
503 | account.AccountType = "Savings"
504 | account.Currency = "GBP"
505 | account.Name = "Foo Bar"
506 |
507 | //Insert will encrypt the account
508 | err = db.Insert(account)
509 | if err != nil {
510 | log.Println(err)
511 | }
512 |
513 | //Get will automatically decrypt account
514 | var account BankAccount
515 | err = db.Get("Accounts/202003/0123456789", &account)
516 | if err != nil {
517 | log.Println(err)
518 | }
519 | }
520 | ```
521 |
522 | ## Resources
523 |
524 | For more information on getting started with Gitdb, check out the following articles:
525 | * [Gtidb - an embedded distributed document database for Go](https://docs.google.com/document/d/1OPalq-7J_Vo_uks35up_4a_V0BsRrpwL1J4JG6ZFoEs/edit?usp=sharing) by Oke Ugwu
526 |
527 |
528 | ## Caveats & Limitations
529 |
530 | It's important to pick the right tool for the job and GitDB is no exception.
531 | Here are a few things to note when evaluating and using GitDB:
532 |
533 | * GitDB is good for systems where data producers are indpendent.
534 | * GitDB currently depends on the git binary to work
535 |
536 | ## Reading the Source
537 |
538 | GitDB is a relatively small code base (<5KLOC) for an embedded, distributed,
539 | document database so it can be a good starting point for people
540 | interested in how databases work.
541 |
542 | The best places to start are the main entry points into GitDB:
543 |
544 | - [`Open()`](https://github.com/gogitdb/gitdb/blob/a8088f138b072edd64021591e34adf878f2d0bd5/init.go#L17) - Initializes the reference to the database. It's responsible for
545 | creating the database if it doesn't exist and pulling down existing database
546 | if an online remote is specified.
547 |
548 | If you have additional notes that could be helpful for others, please submit
549 | them via pull request.
550 |
551 |
552 |
560 |
--------------------------------------------------------------------------------
/all_test.go:
--------------------------------------------------------------------------------
1 | package gitdb_test
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "flag"
7 | "fmt"
8 | "io/ioutil"
9 | "os"
10 | "os/exec"
11 | "path/filepath"
12 | "strconv"
13 | "strings"
14 | "testing"
15 |
16 | "github.com/bouggo/log"
17 | "github.com/gogitdb/gitdb/v2"
18 | )
19 |
20 | var testDb gitdb.GitDb
21 | var messageId int
22 |
23 | const testData = "/tmp/gitdb-test"
24 | const dbPath = testData + "/data"
25 | const fakeRemote = testData + "/online"
26 |
27 | //Test flags for more interactivity
28 | var flagLogLevel int
29 | var flagFakeRemote bool
30 |
31 | func TestMain(m *testing.M) {
32 | flag.IntVar(&flagLogLevel, "loglevel", int(log.LevelTest), "control verbosity of test logs")
33 | flag.BoolVar(&flagFakeRemote, "fakerepo", true, "create fake remote repo for tests")
34 | flag.Parse()
35 |
36 | //fail test if git is not installed
37 | if _, err := exec.LookPath("git"); err != nil {
38 | log.Test("git is required to run tests")
39 | return
40 | }
41 |
42 | gitdb.SetLogLevel(gitdb.LogLevel(flagLogLevel))
43 | m.Run()
44 | }
45 |
46 | func setup(t testing.TB, cfg *gitdb.Config) func(t testing.TB) {
47 |
48 | if cfg == nil {
49 | cfg = getConfig()
50 | }
51 |
52 | if flagFakeRemote && !strings.HasPrefix(cfg.OnlineRemote, "git@") {
53 | fakeOnlineRepo(t)
54 | }
55 |
56 | //if DbPath is pointing to existing test data, create a fake .git folder
57 | //so that it passes the git repo check on db.boot
58 | if strings.HasPrefix(cfg.DBPath, "./testdata") {
59 | if err := os.MkdirAll(filepath.Join(cfg.DBPath, "data", ".git"), 0755); err != nil {
60 | t.Errorf("fake .git failed: %s", err.Error())
61 | }
62 | }
63 |
64 | testDb = getDbConn(t, cfg)
65 | messageId = 0
66 |
67 | testDb.RegisterModel("Message", &Message{})
68 | testDb.RegisterModel("MessageV2", &MessageV2{})
69 |
70 | return teardown
71 | }
72 |
73 | func teardown(t testing.TB) {
74 | testDb.Close()
75 |
76 | log.Test("truncating test data")
77 | if err := os.RemoveAll(testData); err != nil {
78 | t.Errorf("cleanup failed - %s", err.Error())
79 | }
80 | }
81 |
82 | func fakeOnlineRepo(t testing.TB) {
83 | log.Test("creating fake online repo")
84 | if err := os.MkdirAll(fakeRemote, 0755); err != nil {
85 | t.Errorf("fake repo failed: %s", err.Error())
86 | return
87 | }
88 |
89 | cmd := exec.Command("git", "-C", fakeRemote, "init", "--bare")
90 | if _, err := cmd.CombinedOutput(); err != nil {
91 | t.Errorf("fake repo failed: %s", err.Error())
92 | return
93 | }
94 | }
95 |
96 | func getDbConn(t testing.TB, cfg *gitdb.Config) gitdb.GitDb {
97 | conn, err := gitdb.Open(cfg)
98 | if err != nil {
99 | t.Errorf("getDbConn failed: %s", err)
100 | return nil
101 | }
102 | log.Test("test db connection opened")
103 | conn.SetUser(gitdb.NewUser("Tester", "tester@io"))
104 | return conn
105 | }
106 |
107 | func getConfig() *gitdb.Config {
108 | config := gitdb.NewConfig(dbPath)
109 | config.EncryptionKey = "b61ba8270ccc3c1d42b4417e7bd60b71"
110 | if flagFakeRemote {
111 | config.OnlineRemote = fakeRemote
112 | }
113 |
114 | return config
115 | }
116 |
117 | func getMockConfig() *gitdb.Config {
118 | config := gitdb.NewConfig("/mock")
119 | config.Mock = true
120 | return config
121 | }
122 |
123 | func getReadTestConfig(version string) *gitdb.Config {
124 | cfg := gitdb.NewConfig("./testdata/" + version + "/data")
125 | if version == "v1" {
126 | //add a factory method
127 | cfg.Factory = func(dataset string) gitdb.Model {
128 | switch dataset {
129 | case "Message":
130 | return &Message{}
131 | case "MessageV2":
132 | return &MessageV2{}
133 | }
134 | return &Message{}
135 | }
136 | }
137 | return cfg
138 | }
139 |
140 | func getTestMessage() *Message {
141 | m := getTestMessageWithId(messageId)
142 | messageId++
143 |
144 | return m
145 | }
146 |
147 | func getTestMessageWithId(messageId int) *Message {
148 | m := &Message{
149 | MessageId: messageId,
150 | From: "alice@example.com",
151 | To: "bob@example.com",
152 | Body: "Hello",
153 | }
154 |
155 | return m
156 | }
157 |
158 | type Message struct {
159 | gitdb.TimeStampedModel
160 | MessageId int
161 | From string
162 | To string
163 | Body string
164 | }
165 |
166 | func (m *Message) GetSchema() *gitdb.Schema {
167 |
168 | name := "Message"
169 | block := "b0"
170 | // block := gitdb.AutoBlock(dbPath, m, gitdb.BlockByCount, 2)
171 | record := fmt.Sprintf("%d", m.MessageId)
172 |
173 | //Indexes speed up searching
174 | indexes := make(map[string]interface{})
175 | indexes["From"] = m.From
176 |
177 | return gitdb.NewSchema(name, block, record, indexes)
178 | }
179 |
180 | func (m *Message) Validate() error { return nil }
181 | func (m *Message) IsLockable() bool { return true }
182 | func (m *Message) ShouldEncrypt() bool { return true }
183 | func (m *Message) GetLockFileNames() []string {
184 | return []string{
185 | fmt.Sprintf("%d-%s", m.MessageId, m.From),
186 | }
187 | }
188 |
189 | // func (m *Message) BeforeInsert() error { return nil }
190 |
191 | type MessageV2 struct {
192 | gitdb.TimeStampedModel
193 | MessageId int
194 | From string
195 | To string
196 | Body string
197 | }
198 |
199 | func (m *MessageV2) GetSchema() *gitdb.Schema {
200 |
201 | name := "MessageV2"
202 | block := m.CreatedAt.Format("200601")
203 | record := fmt.Sprintf("%d", m.MessageId)
204 |
205 | //Indexes speed up searching
206 | indexes := make(map[string]interface{})
207 | indexes["From"] = m.From
208 |
209 | return gitdb.NewSchema(name, block, record, indexes)
210 | }
211 |
212 | func (m *MessageV2) Validate() error { return nil }
213 | func (m *MessageV2) IsLockable() bool { return false }
214 | func (m *MessageV2) ShouldEncrypt() bool { return false }
215 | func (m *MessageV2) GetLockFileNames() []string { return []string{} }
216 |
217 | // func (m *MessageV2) BeforeInsert() error { return nil }
218 |
219 | //count the number of records in fetched block
220 | func countRecords(dataset string) int {
221 |
222 | datasetPath := getConfig().DBPath + "/data/" + dataset + "/"
223 |
224 | cmd := exec.Command("/bin/bash", "-c", "grep "+dataset+" "+datasetPath+"*.json | wc -l | awk '{print $1}'")
225 | b, err := cmd.CombinedOutput()
226 | if err != nil {
227 | println(err.Error())
228 | }
229 |
230 | v := strings.TrimSpace(string(b))
231 | want, err := strconv.Atoi(v)
232 | if err != nil {
233 | println(v)
234 | println(err.Error())
235 | want = 0
236 | }
237 |
238 | return want
239 | }
240 |
241 | func generateInserts(t testing.TB, count int) {
242 | log.Test(fmt.Sprintf("inserting %d records\n", count))
243 | for i := 0; i < count; i++ {
244 | if err := testDb.Insert(getTestMessage()); err != nil {
245 | t.Errorf("generateInserts failed: %s", err)
246 | }
247 |
248 | }
249 | log.Test("done inserting")
250 | }
251 |
252 | func insert(m gitdb.Model, benchmark bool) error {
253 | if err := testDb.Insert(m); err != nil {
254 | return err
255 | }
256 |
257 | if !benchmark && !m.ShouldEncrypt() {
258 | //check that block file exist
259 | recordID := gitdb.ID(m)
260 | dataset, block, _, err := gitdb.ParseID(recordID)
261 | if err != nil {
262 | return err
263 | }
264 | cfg := getConfig()
265 | blockFile := filepath.Join(cfg.DBPath, "data", dataset, block+".json")
266 | if _, err := os.Stat(blockFile); err != nil {
267 | //return err
268 | return errors.New("insert test stat failed - " + blockFile)
269 | } else {
270 | b, err := ioutil.ReadFile(blockFile)
271 | if err != nil {
272 | return err
273 | }
274 |
275 | rep := strings.NewReplacer("\n", "", "\\", "", "\t", "", "\"{", "{", "}\"", "}", " ", "")
276 | got := rep.Replace(string(b))
277 |
278 | w := map[string]interface{}{
279 | recordID: struct {
280 | Version string
281 | Indexes map[string]interface{}
282 | Data gitdb.Model
283 | }{
284 | gitdb.RecVersion,
285 | gitdb.Indexes(m),
286 | m,
287 | },
288 | }
289 |
290 | x, _ := json.Marshal(w)
291 | want := string(x)
292 |
293 | want = want[1 : len(want)-1]
294 |
295 | if !strings.Contains(got, want) {
296 | return fmt.Errorf("Want: %s, Got: %s", want, got)
297 | }
298 | }
299 | }
300 |
301 | return nil
302 | }
303 |
--------------------------------------------------------------------------------
/cmd/gitdb/gitdb.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/base64"
6 | "flag"
7 | "fmt"
8 | "io/ioutil"
9 | "os"
10 | "path"
11 | "path/filepath"
12 | "runtime"
13 | "strings"
14 | "time"
15 | )
16 |
17 | var (
18 | packageRoot = "../../"
19 |
20 | embedCommand = flag.NewFlagSet("embed", flag.ExitOnError)
21 | output = embedCommand.String("o", "./ui_static.go", "output file name; default ./ui_static.go")
22 |
23 | // dbpath = flag.String("p", "", "path do gitdb")
24 | )
25 |
26 | func main() {
27 |
28 | command := os.Args[1]
29 | switch command {
30 | case "embed-ui":
31 | embedCommand.Parse(os.Args[2:])
32 | err := embedUI()
33 | if err != nil {
34 | fmt.Println(err.Error())
35 | }
36 | default:
37 | fmt.Println("invalid command; try gitdb embed-ui")
38 | //future commands
39 | //clean-db i.e git gc
40 | //repair
41 | //dataset
42 | //dataset blocks
43 | //dataset records
44 | return
45 | }
46 | }
47 |
48 | type staticFile struct {
49 | Name string
50 | Content string
51 | }
52 |
53 | func embedUI() error {
54 | _, filename, _, ok := runtime.Caller(0)
55 | if ok {
56 | packageRoot = path.Dir(path.Dir(path.Dir(filename))) + "/"
57 | }
58 |
59 | var files []staticFile
60 | if err := readAllStaticFiles(filepath.Join(packageRoot, "static"), &files); err != nil {
61 | return err
62 | }
63 |
64 | _, err := os.Stat(path.Dir(*output))
65 | if err != nil {
66 | return err
67 | }
68 |
69 | w, err := os.Create(*output)
70 | if err != nil {
71 | fmt.Println(err)
72 | return err
73 | }
74 |
75 | return packageTmpl.Execute(w, struct {
76 | Files []staticFile
77 | Date string
78 | }{
79 | Files: files,
80 | Date: time.Now().Format(time.RFC1123),
81 | })
82 | }
83 |
84 | func readAllStaticFiles(path string, files *[]staticFile) error {
85 |
86 | dirs, err := ioutil.ReadDir(path)
87 | if err != nil {
88 | return err
89 | }
90 |
91 | for _, dir := range dirs {
92 | fileName := filepath.Join(path, dir.Name())
93 | fmt.Println(fileName)
94 | if !dir.IsDir() {
95 | b, err := ioutil.ReadFile(fileName)
96 | if err != nil {
97 | return err
98 | }
99 |
100 | b = bytes.Replace(b, []byte(" "), []byte(""), -1)
101 | b = bytes.Replace(b, []byte("\n"), []byte(""), -1)
102 |
103 | content := base64.StdEncoding.EncodeToString(b)
104 |
105 | fileKey := strings.Replace(fileName, packageRoot, "", 1)
106 | *files = append(*files, staticFile{fileKey, content})
107 | }
108 |
109 | if !strings.HasPrefix(dir.Name(), ".") && dir.IsDir() {
110 | readAllStaticFiles(fileName, files)
111 | }
112 | }
113 |
114 | return nil
115 | }
116 |
--------------------------------------------------------------------------------
/cmd/gitdb/gitdb_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "testing"
6 | )
7 |
8 | func Test_embedUI(t *testing.T) {
9 | err := embedUI()
10 | if err != nil {
11 | t.Errorf("embedUI() failed: %s", err)
12 | }
13 | os.Remove("./ui_static.go")
14 | }
15 |
--------------------------------------------------------------------------------
/cmd/gitdb/template.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "text/template"
4 |
5 | var packageTmpl = template.Must(template.New("package").Parse(`package gitdb
6 | // Code generated by gitdb embed-ui on {{.Date}}; DO NOT EDIT.
7 |
8 | func init() {
9 | //Embed Files
10 | {{range .Files}}
11 | getFs().embed("{{.Name}}", "{{.Content}}")
12 | {{end}}
13 | }
14 | `))
15 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "errors"
5 | "time"
6 | )
7 |
8 | // Config represents configuration options for GitDB
9 | type Config struct {
10 | ConnectionName string
11 | DBPath string
12 | OnlineRemote string
13 | EncryptionKey string
14 | SyncInterval time.Duration
15 | User *User
16 | Factory func(string) Model
17 | EnableUI bool
18 | UIPort int
19 | // Mock is a hook for testing apps. If true will return a Mock DB connection
20 | Mock bool
21 | Driver dbDriver
22 | }
23 |
24 | const defaultConnectionName = "default"
25 | const defaultSyncInterval = time.Second * 5
26 | const defaultUserName = "ghost"
27 | const defaultUserEmail = "ghost@gitdb.local"
28 | const defaultUIPort = 4120
29 |
30 | // NewConfig constructs a *Config
31 | func NewConfig(dbPath string) *Config {
32 | return &Config{
33 | DBPath: dbPath,
34 | SyncInterval: defaultSyncInterval,
35 | User: NewUser(defaultUserName, defaultUserEmail),
36 | ConnectionName: defaultConnectionName,
37 | UIPort: defaultUIPort,
38 | Driver: &gitDriver{driver: &gitBinaryDriver{}},
39 | }
40 | }
41 |
42 | // NewConfigWithLocalDriver constructs a *Config
43 | func NewConfigWithLocalDriver(dbPath string) *Config {
44 | return &Config{
45 | DBPath: dbPath,
46 | SyncInterval: defaultSyncInterval,
47 | User: NewUser(defaultUserName, defaultUserEmail),
48 | ConnectionName: defaultConnectionName,
49 | UIPort: defaultUIPort,
50 | Driver: &localDriver{},
51 | }
52 | }
53 |
54 | // Validate returns an error is *Config.DBPath is not set
55 | func (c *Config) Validate() error {
56 | if len(c.DBPath) == 0 {
57 | return errors.New("Config.DbPath must be set")
58 | }
59 |
60 | return nil
61 | }
62 |
--------------------------------------------------------------------------------
/db.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "errors"
5 | "os"
6 | "path/filepath"
7 | "sync"
8 | "time"
9 |
10 | "github.com/bouggo/log"
11 | "github.com/gogitdb/gitdb/v2/internal/db"
12 | )
13 |
14 | // RecVersion of gitdb
15 | const RecVersion = "v2"
16 |
17 | // SearchMode defines how gitdb should search with SearchParam
18 | type SearchMode int
19 |
20 | const (
21 | // SearchEquals will search index for records whose values equal SearchParam.Value
22 | SearchEquals SearchMode = 1
23 | // SearchContains will search index for records whose values contain SearchParam.Value
24 | SearchContains SearchMode = 2
25 | // SearchStartsWith will search index for records whose values start with SearchParam.Value
26 | SearchStartsWith SearchMode = 3
27 | // SearchEndsWith will search index for records whose values ends with SearchParam.Value
28 | SearchEndsWith SearchMode = 4
29 | )
30 |
31 | // SearchParam represents search parameters against GitDB index
32 | type SearchParam struct {
33 | Index string
34 | Value string
35 | }
36 |
37 | // GitDb interface defines all exported funcs an implementation must have
38 | type GitDb interface {
39 | Close() error
40 | Insert(m Model) error
41 | InsertMany(m []Model) error
42 | Get(id string, m Model) error
43 | Exists(id string) error
44 | Fetch(dataset string, block ...string) ([]*db.Record, error)
45 | Search(dataDir string, searchParams []*SearchParam, searchMode SearchMode) ([]*db.Record, error)
46 | Delete(id string) error
47 | DeleteOrFail(id string) error
48 | Lock(m Model) error
49 | Unlock(m Model) error
50 | Upload() *Upload
51 | Migrate(from Model, to Model) error
52 | GetMails() []*mail
53 | StartTransaction(name string) Transaction
54 | GetLastCommitTime() (time.Time, error)
55 | SetUser(user *User) error
56 | Config() Config
57 | Sync() error
58 | RegisterModel(dataset string, m Model) bool
59 | }
60 |
61 | type gitdb struct {
62 | mu sync.Mutex
63 | indexMu sync.Mutex
64 | writeMu sync.Mutex
65 | syncMu sync.Mutex
66 | commit sync.WaitGroup
67 | locked chan bool
68 | shutdown chan bool
69 | events chan *dbEvent
70 |
71 | config Config
72 | driver dbDriver
73 |
74 | autoCommit bool
75 | indexUpdated bool
76 | loopStarted bool
77 | closed bool
78 |
79 | indexCache gdbSimpleIndexCache
80 | loadedBlocks map[string]*db.Block
81 |
82 | mails []*mail
83 | registry map[string]Model
84 | }
85 |
86 | func newConnection() *gitdb {
87 | // autocommit defaults to true
88 | db := &gitdb{autoCommit: true, indexCache: make(gdbSimpleIndexCache)}
89 | // initialize channels
90 | db.events = make(chan *dbEvent, 1)
91 | db.locked = make(chan bool, 1)
92 | // initialize shutdown channel with capacity 3
93 | // to represent the event loop, sync clock, UI server
94 | // goroutines
95 | db.shutdown = make(chan bool, 3)
96 |
97 | return db
98 | }
99 |
100 | func (g *gitdb) Config() Config {
101 | return g.config
102 | }
103 |
104 | func (g *gitdb) Close() error {
105 | if g == nil {
106 | return errors.New("gitdb is nil")
107 | }
108 |
109 | g.mu.Lock()
110 | defer g.mu.Unlock()
111 | log.Test("shutting down gitdb")
112 | if g.closed {
113 | log.Test("connection already closed")
114 | return nil
115 | }
116 |
117 | // flush index to disk
118 | if err := g.flushIndex(); err != nil {
119 | return err
120 | }
121 |
122 | // send shutdown event to event loop and sync clock
123 | g.shutdown <- true
124 | g.waitForCommit()
125 |
126 | // remove cached connection
127 | delete(conns, g.config.ConnectionName)
128 | g.closed = true
129 | log.Info("closed gitdb conn")
130 | return nil
131 | }
132 |
133 | func (g *gitdb) configure(cfg Config) {
134 | if len(cfg.ConnectionName) == 0 {
135 | cfg.ConnectionName = defaultConnectionName
136 | }
137 |
138 | if int64(cfg.SyncInterval) == 0 {
139 | cfg.SyncInterval = defaultSyncInterval
140 | }
141 |
142 | if cfg.UIPort == 0 {
143 | cfg.UIPort = defaultUIPort
144 | }
145 |
146 | g.driver = cfg.Driver
147 | if cfg.Driver == nil {
148 | g.driver = &gitDriver{driver: &gitBinaryDriver{}}
149 | }
150 |
151 | g.config = cfg
152 | }
153 |
154 | // Migrate model from one schema to another
155 | func (g *gitdb) Migrate(from Model, to Model) error {
156 |
157 | // TODO add test case for this
158 | // schema has not changed
159 | /*if from.GetSchema().recordID() == to.GetSchema().recordID() {
160 | return errors.New("Invalid migration - no change found in schema")
161 | }*/
162 |
163 | block := db.NewEmptyBlock(g.config.EncryptionKey)
164 | if err := g.doFetch(from.GetSchema().name(), block); err != nil {
165 | return err
166 | }
167 |
168 | oldBlocks := map[string]string{}
169 | var migrate []Model
170 | for _, record := range block.Records() {
171 | dataset, blockID, _, _ := ParseID(record.ID())
172 | if _, ok := oldBlocks[blockID]; !ok {
173 | blockFilePath := filepath.Join(g.dbDir(), dataset, blockID+".json")
174 | oldBlocks[blockID] = blockFilePath
175 | }
176 |
177 | if err := record.Hydrate(to); err != nil {
178 | return err
179 | }
180 |
181 | migrate = append(migrate, to)
182 | }
183 |
184 | // InsertMany will rollback if any insert fails
185 | if err := g.InsertMany(migrate); err != nil {
186 | return err
187 | }
188 |
189 | // remove all old block files
190 | for _, blockFilePath := range oldBlocks {
191 | log.Info("Removing old block: " + blockFilePath)
192 | err := os.Remove(blockFilePath)
193 | if err != nil {
194 | return err
195 | }
196 | }
197 |
198 | return nil
199 | }
200 |
201 | func (g *gitdb) GetLastCommitTime() (time.Time, error) {
202 | return g.driver.lastCommitTime()
203 | }
204 |
--------------------------------------------------------------------------------
/db_mock.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "reflect"
8 | "strings"
9 | "time"
10 |
11 | "github.com/bouggo/log"
12 | "github.com/gogitdb/gitdb/v2/internal/db"
13 | )
14 |
15 | type mockdb struct {
16 | config Config
17 | data map[string]Model
18 | index map[string]map[string]interface{}
19 | locks map[string]bool
20 | }
21 |
22 | type mocktransaction struct {
23 | name string
24 | operations []operation
25 | db *mockdb
26 | }
27 |
28 | func (t *mocktransaction) Commit() error {
29 | for _, o := range t.operations {
30 | if err := o(); err != nil {
31 | log.Info("Reverting transaction: " + err.Error())
32 | return err
33 | }
34 | }
35 | return nil
36 | }
37 |
38 | func (t *mocktransaction) AddOperation(o operation) {
39 | t.operations = append(t.operations, o)
40 | }
41 |
42 | func newMockConnection() *mockdb {
43 | db := &mockdb{
44 | data: make(map[string]Model),
45 | index: make(map[string]map[string]interface{}),
46 | locks: make(map[string]bool),
47 | }
48 | return db
49 | }
50 |
51 | func (g *mockdb) Close() error {
52 | g.data = nil
53 | return nil
54 | }
55 |
56 | func (g *mockdb) Insert(m Model) error {
57 | g.data[ID(m)] = m
58 |
59 | for name, value := range m.GetSchema().indexes {
60 | key := m.GetSchema().dataset + "." + name
61 | if _, ok := g.index[key]; !ok {
62 | g.index[key] = make(map[string]interface{})
63 | }
64 | g.index[key][ID(m)] = value
65 | }
66 |
67 | return nil
68 | }
69 |
70 | func (g *mockdb) InsertMany(m []Model) error {
71 | for _, model := range m {
72 | g.Insert(model)
73 | }
74 | return nil
75 | }
76 |
77 | func (g *mockdb) Get(id string, result Model) error {
78 |
79 | if reflect.ValueOf(result).Kind() != reflect.Ptr || reflect.ValueOf(result).IsNil() {
80 | return errors.New("Second argument to Get must be a non-nil pointer")
81 | }
82 |
83 | model, exists := g.data[id]
84 | if exists {
85 |
86 | resultType := reflect.ValueOf(result).Type()
87 | modelType := reflect.ValueOf(model).Type()
88 | if resultType != modelType {
89 | return fmt.Errorf("Second argument to Get must be of type %v", modelType)
90 | }
91 |
92 | b, err := json.Marshal(model)
93 | if err != nil {
94 | return err
95 | }
96 | return json.Unmarshal(b, result)
97 | }
98 |
99 | dataset, _, _, _ := ParseID(id)
100 | return fmt.Errorf("Record %s not found in %s", id, dataset)
101 | }
102 |
103 | func (g *mockdb) Exists(id string) error {
104 | _, exists := g.data[id]
105 | if !exists {
106 | dataset, _, _, _ := ParseID(id)
107 | return fmt.Errorf("Record %s not found in %s", id, dataset)
108 | }
109 |
110 | return nil
111 | }
112 |
113 | func (g *mockdb) Fetch(dataset string, blocks ...string) ([]*db.Record, error) {
114 | var result []*db.Record
115 | blockStream := "|" + strings.Join(blocks, "|") + "|"
116 | for id, model := range g.data {
117 | ds, b, _, err := ParseID(id)
118 | if err != nil {
119 | log.Error(err.Error())
120 | continue
121 | }
122 |
123 | if len(blocks) > 0 {
124 | if ds == dataset && strings.Contains(blockStream, "|"+b+"|") {
125 | result = append(result, db.ConvertModel(ID(model), model))
126 | }
127 | } else {
128 | if ds == dataset {
129 | result = append(result, db.ConvertModel(ID(model), model))
130 | }
131 | }
132 | }
133 |
134 | return result, nil
135 | }
136 |
137 | func (g *mockdb) Search(dataset string, searchParams []*SearchParam, searchMode SearchMode) ([]*db.Record, error) {
138 | result := []*db.Record{}
139 | for _, searchParam := range searchParams {
140 | key := dataset + "." + searchParam.Index
141 |
142 | queryValue := strings.ToLower(searchParam.Value)
143 | for recordID, value := range g.index[key] {
144 | addResult := false
145 | dbValue := strings.ToLower(value.(string))
146 | switch searchMode {
147 | case SearchEquals:
148 | addResult = dbValue == queryValue
149 | case SearchContains:
150 | addResult = strings.Contains(dbValue, queryValue)
151 | case SearchStartsWith:
152 | addResult = strings.HasPrefix(dbValue, queryValue)
153 | case SearchEndsWith:
154 | addResult = strings.HasSuffix(dbValue, queryValue)
155 | }
156 |
157 | if addResult {
158 | result = append(result, db.ConvertModel(recordID, g.data[recordID]))
159 | }
160 | }
161 | }
162 |
163 | return result, nil
164 | }
165 |
166 | func (g *mockdb) Delete(id string) error {
167 | delete(g.data, id)
168 | return nil
169 | }
170 |
171 | func (g *mockdb) DeleteOrFail(id string) error {
172 | _, exists := g.data[id]
173 | if !exists {
174 | return fmt.Errorf("record %s does not exist", id)
175 | }
176 |
177 | delete(g.data, id)
178 | return nil
179 | }
180 |
181 | func (g *mockdb) Lock(m Model) error {
182 |
183 | if _, ok := m.(LockableModel); !ok {
184 | return errors.New("Model is not lockable")
185 | }
186 |
187 | for _, l := range m.(LockableModel).GetLockFileNames() {
188 | key := m.GetSchema().dataset + "." + l
189 | if _, ok := g.locks[l]; !ok {
190 | g.locks[key] = true
191 | } else {
192 | return errors.New("Lock file already exist: " + l)
193 | }
194 | }
195 | return nil
196 | }
197 |
198 | func (g *mockdb) Unlock(m Model) error {
199 | if _, ok := m.(LockableModel); !ok {
200 | return errors.New("Model is not lockable")
201 | }
202 |
203 | for _, l := range m.(LockableModel).GetLockFileNames() {
204 | key := m.GetSchema().dataset + "." + l
205 | delete(g.locks, key)
206 | }
207 | return nil
208 | }
209 |
210 | func (g *mockdb) Upload() *Upload {
211 | //todo
212 | return nil
213 | }
214 |
215 | func (g *mockdb) GetMails() []*mail {
216 | return []*mail{}
217 | }
218 |
219 | func (g *mockdb) StartTransaction(name string) Transaction {
220 | //todo return mock transaction
221 | return &mocktransaction{name: name, db: g}
222 | }
223 |
224 | func (g *mockdb) GetLastCommitTime() (time.Time, error) {
225 | return time.Now(), nil
226 | }
227 |
228 | func (g *mockdb) SetUser(user *User) error {
229 | g.config.User = user
230 | return nil
231 | }
232 |
233 | func (g *mockdb) Migrate(from Model, to Model) error {
234 |
235 | migrate := []Model{}
236 | records, err := g.Fetch(from.GetSchema().dataset)
237 | if err != nil {
238 | return err
239 | }
240 |
241 | for _, record := range records {
242 | if err := record.Hydrate(to); err != nil {
243 | return err
244 | }
245 |
246 | migrate = append(migrate, to)
247 | }
248 |
249 | if err := g.InsertMany(migrate); err != nil {
250 | return err
251 | }
252 |
253 | return nil
254 | }
255 |
256 | func (g *mockdb) Config() Config {
257 | return g.config
258 | }
259 |
260 | func (g *mockdb) configure(cfg Config) {
261 | if len(cfg.ConnectionName) == 0 {
262 | cfg.ConnectionName = defaultConnectionName
263 | }
264 | g.config = cfg
265 | }
266 |
267 | func (g *mockdb) Sync() error {
268 | return nil
269 | }
270 |
271 | func (g *mockdb) RegisterModel(dataset string, m Model) bool {
272 | return true
273 | }
274 |
--------------------------------------------------------------------------------
/db_mock_test.go:
--------------------------------------------------------------------------------
1 | package gitdb_test
2 |
3 | import (
4 | "errors"
5 | "reflect"
6 | "testing"
7 |
8 | "github.com/gogitdb/gitdb/v2"
9 | )
10 |
11 | func setupMock(t *testing.T) gitdb.GitDb {
12 | cfg := getMockConfig()
13 | db, err := gitdb.Open(cfg)
14 | if err != nil {
15 | t.Error("mock db setup failed")
16 | }
17 |
18 | //insert some test models for testing read operations
19 | i := 101
20 | for i < 111 {
21 | m := getTestMessageWithId(i)
22 | db.Insert(m)
23 | i++
24 | }
25 |
26 | return db
27 | }
28 |
29 | func TestMockClose(t *testing.T) {
30 | db := setupMock(t)
31 | if err := db.Close(); err != nil {
32 | t.Errorf("db.Close() failed: %s", err)
33 | }
34 | }
35 |
36 | func TestMockInsert(t *testing.T) {
37 | db := setupMock(t)
38 | m := getTestMessage()
39 |
40 | if err := db.Insert(m); err != nil {
41 | t.Errorf("db.Insert failed: %s", err)
42 | }
43 | }
44 |
45 | func TestMockInsertMany(t *testing.T) {
46 |
47 | db := setupMock(t)
48 | msgs := []gitdb.Model{}
49 | for i := 0; i < 10; i++ {
50 | m := getTestMessage()
51 | msgs = append(msgs, m)
52 | }
53 |
54 | err := db.InsertMany(msgs)
55 | if err != nil {
56 | t.Errorf("db.InsertMany failed: %s", err)
57 | }
58 | }
59 |
60 | func TestMockGet(t *testing.T) {
61 | db := setupMock(t)
62 |
63 | m := &Message{}
64 |
65 | id := "Message/b0/1"
66 | if err := db.Get(id, m); err == nil {
67 | t.Errorf("db.Get(%s) failed: %s", id, err)
68 | }
69 |
70 | id = "Message/b0/110"
71 | if err := db.Get(id, m); err != nil {
72 | t.Errorf("db.Get(%s) failed: %s", id, err)
73 | }
74 |
75 | if mId := gitdb.ID(m); mId != id {
76 | t.Errorf("db.Get(%s) want: %s, got: %s", id, id, mId)
77 | }
78 | }
79 |
80 | func TestMockExists(t *testing.T) {
81 | db := setupMock(t)
82 |
83 | id := "Message/b0/1"
84 | if err := db.Exists(id); err == nil {
85 | t.Errorf("db.Get(%s) failed: %s", id, err)
86 | }
87 |
88 | id = "Message/b0/110"
89 | if err := db.Exists(id); err != nil {
90 | t.Errorf("db.Get(%s) failed: %s", id, err)
91 | }
92 | }
93 |
94 | func TestMockFetch(t *testing.T) {
95 | db := setupMock(t)
96 |
97 | dataset := "Message"
98 | records, err := db.Fetch(dataset)
99 | if err != nil {
100 | t.Errorf("db.Fetch(%s) failed: %s", dataset, err)
101 | }
102 |
103 | want := 10
104 | if got := len(records); got != want {
105 | t.Errorf("db.Fetch(%s) failed: want %d, got %d", dataset, want, got)
106 | }
107 | }
108 |
109 | func TestMockFetchBlock(t *testing.T) {
110 | db := setupMock(t)
111 |
112 | dataset := "Message"
113 | records, err := db.Fetch(dataset, "b0")
114 | if err != nil {
115 | t.Errorf("db.Fetch(%s) failed: %s", dataset, err)
116 | }
117 |
118 | want := 10
119 | if got := len(records); got != want {
120 | t.Errorf("db.Fetch(%s) failed: want %d, got %d", dataset, want, got)
121 | }
122 | }
123 |
124 | func TestMockSearch(t *testing.T) {
125 | db := setupMock(t)
126 |
127 | count := 10
128 | sp := &gitdb.SearchParam{
129 | Index: "From",
130 | Value: "alice@example.com",
131 | }
132 |
133 | results, err := db.Search("Message", []*gitdb.SearchParam{sp}, gitdb.SearchEquals)
134 | if err != nil {
135 | t.Errorf("search failed with error - %s", err)
136 | }
137 |
138 | if len(results) != count {
139 | t.Errorf("search result count wrong. want: %d, got: %d", count, len(results))
140 | }
141 | }
142 |
143 | func TestMockDelete(t *testing.T) {
144 | db := setupMock(t)
145 |
146 | //delete non-existent record should pass
147 | id := "Message/b0/1"
148 | if err := db.Delete(id); err != nil {
149 | t.Errorf("db.Delete(%s) failed: %s", id, err)
150 | }
151 |
152 | //delete existent record should pass
153 | id = "Message/b0/110"
154 | if err := db.Delete(id); err != nil {
155 | t.Errorf("db.Delete(%s) failed: %s", id, err)
156 | }
157 | }
158 | func TestMockDeleteOrFail(t *testing.T) {
159 | db := setupMock(t)
160 |
161 | //delete non-existent record should fail
162 | id := "Message/b0/1"
163 | if err := db.DeleteOrFail(id); err == nil {
164 | t.Errorf("db.DeleteOrFail(%s) failed: %s", id, err)
165 | }
166 |
167 | id = "Message/b0/110"
168 | if err := db.DeleteOrFail(id); err != nil {
169 | t.Errorf("db.DeleteOrFail(%s) failed: %s", id, err)
170 | }
171 | }
172 | func TestMockLock(t *testing.T) {
173 | db := setupMock(t)
174 |
175 | m := getTestMessage()
176 | if err := db.Lock(m); err != nil {
177 | t.Errorf("db.Lock(m) failed: %s", err)
178 | }
179 | }
180 |
181 | func TestMockUnlock(t *testing.T) {
182 | db := setupMock(t)
183 |
184 | m := getTestMessage()
185 | if err := db.Unlock(m); err != nil {
186 | t.Errorf("db.Lock(m) failed: %s", err)
187 | }
188 | }
189 |
190 | func TestMockMigrate(t *testing.T) {
191 | db := setupMock(t)
192 |
193 | m := &Message{}
194 | m2 := &MessageV2{}
195 |
196 | if err := db.Migrate(m, m2); err != nil {
197 | t.Errorf("db.Migrate(m, m2) returned error - %s", err)
198 | }
199 | }
200 |
201 | func TestMockGetMails(t *testing.T) {
202 | db := setupMock(t)
203 | mails := db.GetMails()
204 | if len(mails) > 0 {
205 | t.Errorf("db.GetMails() should be 0")
206 | }
207 | }
208 |
209 | func TestMockStartTransaction(t *testing.T) {
210 | db := setupMock(t)
211 | tx := db.StartTransaction("test")
212 | if tx == nil {
213 | t.Errorf("db.StartTransaction() returned: %v", nil)
214 | }
215 |
216 | tx.AddOperation(func() error { return nil })
217 | tx.AddOperation(func() error { return nil })
218 | tx.AddOperation(func() error { return errors.New("test error") })
219 | tx.AddOperation(func() error { return nil })
220 | if err := tx.Commit(); err == nil {
221 | t.Error("transaction should fail on 3rd operation")
222 | }
223 | }
224 |
225 | func TestMockGetLastCommitTime(t *testing.T) {
226 | db := setupMock(t)
227 | if _, err := db.GetLastCommitTime(); err != nil {
228 | t.Errorf("db.GetLastCommitTime() returned error - %s", err)
229 | }
230 | }
231 |
232 | func TestMockSetUser(t *testing.T) {
233 | db := setupMock(t)
234 |
235 | user := gitdb.NewUser("test", "tester@gitdb.io")
236 | if err := db.SetUser(user); err != nil {
237 | t.Errorf("db.SetUser failed: %s", err)
238 | }
239 | }
240 |
241 | func TestMockConfig(t *testing.T) {
242 | db := setupMock(t)
243 | // cfg := getMockConfig()
244 | if reflect.DeepEqual(db.Config(), getMockConfig()) {
245 | t.Errorf("db.Config != getMockConfig()")
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/db_test.go:
--------------------------------------------------------------------------------
1 | package gitdb_test
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 |
7 | "github.com/gogitdb/gitdb/v2"
8 | )
9 |
10 | func TestMigrate(t *testing.T) {
11 | teardown := setup(t, nil)
12 | defer teardown(t)
13 |
14 | m := getTestMessageWithId(0)
15 | if err := insert(m, false); err != nil {
16 | t.Errorf("insert failed: %s", err)
17 | }
18 |
19 | m2 := &MessageV2{}
20 |
21 | if err := testDb.Migrate(m, m2); err != nil {
22 | t.Errorf("testDb.Migrate() returned error - %s", err)
23 | }
24 | }
25 |
26 | func TestNewConfig(t *testing.T) {
27 | cfg := gitdb.NewConfig(dbPath)
28 | db, err := gitdb.Open(cfg)
29 | if err != nil {
30 | t.Errorf("gitdb.Open failed: %s", err)
31 | }
32 |
33 | if reflect.DeepEqual(db.Config(), cfg) {
34 | t.Errorf("Config does not match. want: %v, got: %v", cfg, db.Config())
35 | }
36 | }
37 |
38 | func TestNewConfigWithLocalDriver(t *testing.T) {
39 | cfg := gitdb.NewConfigWithLocalDriver(dbPath)
40 | db, err := gitdb.Open(cfg)
41 | if err != nil {
42 | t.Errorf("gitdb.Open failed: %s", err)
43 | }
44 |
45 | if reflect.DeepEqual(db.Config(), cfg) {
46 | t.Errorf("Config does not match. want: %v, got: %v", cfg, db.Config())
47 | }
48 | }
49 |
50 | func TestConfigValidate(t *testing.T) {
51 | cfg := &gitdb.Config{}
52 | if err := cfg.Validate(); err == nil {
53 | t.Errorf("cfg.Validate should fail if DbPath is %s", cfg.DBPath)
54 | }
55 | }
56 |
57 | func TestGetLastCommitTime(t *testing.T) {
58 | teardown := setup(t, nil)
59 | defer teardown(t)
60 |
61 | _, err := testDb.GetLastCommitTime()
62 | if err == nil {
63 | t.Errorf("dbConn.GetLastCommitTime() returned error - %s", err)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/driver.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import "time"
4 |
5 | type dbDriver interface {
6 | name() string
7 | setup(db *gitdb) error
8 | sync() error
9 | commit(filePath string, msg string, user *User) error
10 | undo() error
11 | changedFiles() []string
12 | lastCommitTime() (time.Time, error)
13 | }
14 |
15 | type gitDBDriver interface {
16 | dbDriver
17 | init() error
18 | clone() error
19 | addRemote() error
20 | pull() error
21 | push() error
22 | }
23 |
--------------------------------------------------------------------------------
/driver_git.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "os"
7 | "path/filepath"
8 | "strings"
9 | "time"
10 |
11 | "github.com/bouggo/log"
12 | )
13 |
14 | type gitDriver struct {
15 | driver gitDBDriver
16 | absDBPath string
17 | }
18 |
19 | func (d *gitDriver) name() string {
20 | return d.driver.name()
21 | }
22 |
23 | // if .db directory does not exist, create it and attempt
24 | // to do a git clone from remote
25 | func (d *gitDriver) setup(db *gitdb) error {
26 | d.absDBPath = db.dbDir()
27 | if err := d.driver.setup(db); err != nil {
28 | return err
29 | }
30 |
31 | // create .ssh dir
32 | if err := db.generateSSHKeyPair(); err != nil {
33 | return err
34 | }
35 |
36 | // force git to only use generated ssh key and not fallback to ssh_config or ssh-agent
37 | sshCmd := fmt.Sprintf("ssh -F none -i '%s' -o IdentitiesOnly=yes -o StrictHostKeyChecking=no", db.privateKeyFilePath())
38 | if err := os.Setenv("GIT_SSH_COMMAND", sshCmd); err != nil {
39 | return err
40 | }
41 |
42 | dataDir := db.dbDir()
43 | dotGitDir := filepath.Join(dataDir, ".git")
44 | if _, err := os.Stat(dataDir); err != nil {
45 | log.Info("database not initialized")
46 |
47 | // create db directory
48 | if err := os.MkdirAll(dataDir, 0755); err != nil {
49 | return err
50 | }
51 |
52 | if len(db.config.OnlineRemote) > 0 {
53 | if err := d.clone(); err != nil {
54 | return err
55 | }
56 |
57 | if err := d.addRemote(); err != nil {
58 | return err
59 | }
60 | } else if err := d.init(); err != nil {
61 | return err
62 | }
63 | } else if _, err := os.Stat(dotGitDir); err != nil {
64 | log.Info(err.Error())
65 | return errors.New(db.config.DBPath + " is not a git repository")
66 | } else if len(db.config.OnlineRemote) > 0 { // TODO Review this properly
67 | // if remote is configured i.e stat .git/refs/remotes/online
68 | // if remote dir does not exist add remotes
69 | remotesPath := filepath.Join(dataDir, ".git", "refs", "remotes", "online")
70 | if _, err := os.Stat(remotesPath); err != nil {
71 | if err := d.addRemote(); err != nil {
72 | return err
73 | }
74 | }
75 | }
76 |
77 | return nil
78 | }
79 |
80 | // this function is only called once. I.e when a initializing the database for the
81 | // very first time. In this case we must clone the online repo
82 | func (d *gitDriver) init() error {
83 | // we take this very seriously
84 | if err := d.driver.init(); err != nil {
85 | if err := os.RemoveAll(d.absDBPath); err != nil {
86 | return err
87 | }
88 | }
89 |
90 | return nil
91 | }
92 |
93 | func (d *gitDriver) clone() error {
94 | // we take this very seriously
95 | log.Info("cloning down database...")
96 | if err := d.driver.clone(); err != nil {
97 | // TODO if err is authentication related generate key pair
98 | if err := os.RemoveAll(d.absDBPath); err != nil {
99 | return err
100 | }
101 |
102 | if strings.Contains(err.Error(), "denied") {
103 | return ErrAccessDenied
104 | }
105 | return err
106 | }
107 |
108 | return nil
109 | }
110 |
111 | func (d *gitDriver) addRemote() error {
112 | // we take this very seriously
113 | if err := d.driver.addRemote(); err != nil {
114 | if !strings.Contains(err.Error(), "already exists") {
115 | if err := os.RemoveAll(d.absDBPath); err != nil { // TODO is this necessary?
116 | return err
117 | }
118 | return err
119 | }
120 | }
121 |
122 | return nil
123 | }
124 |
125 | func (d *gitDriver) sync() error {
126 | return d.driver.sync()
127 | }
128 |
129 | func (d *gitDriver) commit(filePath string, msg string, user *User) error {
130 | mu.Lock()
131 | defer mu.Unlock()
132 | if err := d.driver.commit(filePath, msg, user); err != nil {
133 | // todo: update to return this error but for now at least log it
134 | log.Error(err.Error())
135 | }
136 |
137 | return nil
138 | }
139 |
140 | func (d *gitDriver) undo() error {
141 | return d.driver.undo()
142 | }
143 |
144 | func (d *gitDriver) changedFiles() []string {
145 | return d.driver.changedFiles()
146 | }
147 |
148 | func (d *gitDriver) lastCommitTime() (time.Time, error) {
149 | return d.driver.lastCommitTime()
150 | }
151 |
--------------------------------------------------------------------------------
/driver_gitbinary.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "errors"
5 | "os/exec"
6 | "strings"
7 | "time"
8 |
9 | "github.com/bouggo/log"
10 | )
11 |
12 | type gitBinaryDriver struct {
13 | config Config
14 | absDBPath string
15 | }
16 |
17 | func (d *gitBinaryDriver) name() string {
18 | return "gitBinary"
19 | }
20 |
21 | func (d *gitBinaryDriver) setup(db *gitdb) error {
22 | d.config = db.config
23 | d.absDBPath = db.dbDir()
24 | return nil
25 | }
26 |
27 | func (d *gitBinaryDriver) init() error {
28 | cmd := exec.Command("git", "-C", d.absDBPath, "init")
29 | // log(utils.CmdToString(cmd))
30 | if out, err := cmd.CombinedOutput(); err != nil {
31 | log.Info(string(out))
32 | return err
33 | }
34 |
35 | return nil
36 | }
37 |
38 | func (d *gitBinaryDriver) clone() error {
39 |
40 | cmd := exec.Command("git", "clone", "--depth", "10", d.config.OnlineRemote, d.absDBPath)
41 | if out, err := cmd.CombinedOutput(); err != nil {
42 | return errors.New(string(out))
43 | }
44 |
45 | return nil
46 | }
47 |
48 | func (d *gitBinaryDriver) addRemote() error {
49 | // check to see if we have origin / online remotes
50 | cmd := exec.Command("git", "-C", d.absDBPath, "remote")
51 | out, err := cmd.CombinedOutput()
52 |
53 | if err != nil {
54 | // log(string(out))
55 | return err
56 | }
57 |
58 | remoteStr := string(out)
59 | hasOriginRemote := strings.Contains(remoteStr, "origin")
60 | hasOnlineRemote := strings.Contains(remoteStr, "online")
61 |
62 | if hasOriginRemote {
63 | cmd := exec.Command("git", "-C", d.absDBPath, "remote", "rm", "origin")
64 | if out, err := cmd.CombinedOutput(); err != nil {
65 | log.Info(string(out))
66 | }
67 | }
68 |
69 | if !hasOnlineRemote {
70 | cmd = exec.Command("git", "-C", d.absDBPath, "remote", "add", "online", d.config.OnlineRemote)
71 | // log(utils.CmdToString(cmd))
72 | if out, err := cmd.CombinedOutput(); err != nil {
73 | return errors.New(string(out))
74 | }
75 | }
76 |
77 | return nil
78 | }
79 |
80 | func (d *gitBinaryDriver) sync() error {
81 | if err := d.pull(); err != nil {
82 | return err
83 | }
84 | if err := d.push(); err != nil {
85 | return err
86 | }
87 |
88 | return nil
89 | }
90 |
91 | func (d *gitBinaryDriver) pull() error {
92 | cmd := exec.Command("git", "-C", d.absDBPath, "pull", "online", "master")
93 | // log(utils.CmdToString(cmd))
94 | if out, err := cmd.CombinedOutput(); err != nil {
95 | log.Error(string(out))
96 |
97 | return errors.New("failed to pull data from online remote")
98 | }
99 |
100 | return nil
101 | }
102 |
103 | func (d *gitBinaryDriver) push() error {
104 | cmd := exec.Command("git", "-C", d.absDBPath, "push", "online", "master")
105 | // log(utils.CmdToString(cmd))
106 | if out, err := cmd.CombinedOutput(); err != nil {
107 | log.Error(string(out))
108 | return errors.New("failed to push data to online remotes")
109 | }
110 |
111 | return nil
112 | }
113 |
114 | func (d *gitBinaryDriver) commit(filePath string, msg string, user *User) error {
115 | cmd := exec.Command("git", "-C", d.absDBPath, "config", "user.email", user.Email)
116 | // log(utils.CmdToString(cmd))
117 | if out, err := cmd.CombinedOutput(); err != nil {
118 | log.Error(string(out))
119 | return err
120 | }
121 |
122 | cmd = exec.Command("git", "-C", d.absDBPath, "config", "user.name", user.Name)
123 | // log(utils.CmdToString(cmd))
124 | if out, err := cmd.CombinedOutput(); err != nil {
125 | log.Error(string(out))
126 | return err
127 | }
128 |
129 | cmd = exec.Command("git", "-C", d.absDBPath, "add", filePath)
130 | // log(utils.CmdToString(cmd))
131 | if out, err := cmd.CombinedOutput(); err != nil {
132 | log.Error(string(out))
133 | return err
134 | }
135 |
136 | cmd = exec.Command("git", "-C", d.absDBPath, "commit", "-am", msg)
137 | // log(utils.CmdToString(cmd))
138 | if out, err := cmd.CombinedOutput(); err != nil {
139 | log.Error(string(out))
140 | return err
141 | }
142 |
143 | log.Info("new changes committed")
144 | return nil
145 | }
146 |
147 | func (d *gitBinaryDriver) undo() error {
148 | cmd := exec.Command("git", "-C", d.absDBPath, "checkout", ".")
149 | // log(utils.CmdToString(cmd))
150 | if out, err := cmd.CombinedOutput(); err != nil {
151 | log.Error(string(out))
152 | return err
153 | }
154 |
155 | cmd = exec.Command("git", "-C", d.absDBPath, "clean", "-fd")
156 | // log(utils.CmdToString(cmd))
157 | if out, err := cmd.CombinedOutput(); err != nil {
158 | log.Error(string(out))
159 | return err
160 | }
161 |
162 | log.Info("changes reverted")
163 | return nil
164 | }
165 |
166 | func (d *gitBinaryDriver) changedFiles() []string {
167 | var files []string
168 | if len(d.config.OnlineRemote) > 0 {
169 | log.Test("getting list of changed files...")
170 | // git fetch
171 | cmd := exec.Command("git", "-C", d.absDBPath, "fetch", "online", "master")
172 | if out, err := cmd.CombinedOutput(); err != nil {
173 | log.Error(string(out))
174 | return files
175 | }
176 |
177 | // git diff --name-only ..online/master
178 | cmd = exec.Command("git", "-C", d.absDBPath, "diff", "--name-only", "..online/master")
179 | out, err := cmd.CombinedOutput()
180 | if err != nil {
181 | log.Error(string(out))
182 | return files
183 | }
184 |
185 | output := string(out)
186 | if len(output) > 0 {
187 | // strip out lock files
188 | for _, file := range strings.Split(output, "\n") {
189 | if strings.HasSuffix(file, ".json") {
190 | files = append(files, file)
191 | }
192 | }
193 |
194 | return files
195 | }
196 | }
197 |
198 | return files
199 | }
200 |
201 | func (d *gitBinaryDriver) lastCommitTime() (time.Time, error) {
202 | var t time.Time
203 | cmd := exec.Command("git", "-C", d.absDBPath, "log", "-1", "--remotes=online", "--format=%cd", "--date=iso")
204 | // log.PutInfo(utils.CmdToString(cmd))
205 | out, err := cmd.CombinedOutput()
206 | if err != nil {
207 | // log.PutError("gitLastCommit Failed")
208 | return t, err
209 | }
210 |
211 | timeString := string(out)
212 | if len(timeString) >= 25 {
213 | return time.Parse("2006-01-02 15:04:05 -0700", timeString[:25])
214 | }
215 |
216 | return t, errors.New("no commit history in repo")
217 | }
218 |
--------------------------------------------------------------------------------
/driver_local.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "errors"
5 | "os"
6 | "time"
7 |
8 | "github.com/bouggo/log"
9 | )
10 |
11 | type localDriver struct {
12 | config Config
13 | absDBPath string
14 | }
15 |
16 | func (d *localDriver) name() string {
17 | return "local"
18 | }
19 |
20 | func (d *localDriver) setup(db *gitdb) error {
21 | d.config = db.config
22 | d.absDBPath = db.dbDir()
23 | // create db directory
24 | if err := os.MkdirAll(db.dbDir(), 0755); err != nil {
25 | return err
26 | }
27 | return nil
28 | }
29 |
30 | func (d *localDriver) sync() error {
31 | return nil
32 | }
33 |
34 | func (d *localDriver) commit(filePath string, msg string, user *User) error {
35 | log.Info("new changes committed")
36 | return nil
37 | }
38 |
39 | func (d *localDriver) undo() error {
40 | log.Info("changes reverted")
41 | return nil
42 | }
43 |
44 | func (d *localDriver) changedFiles() []string {
45 | var files []string
46 | return files
47 | }
48 |
49 | func (d *localDriver) lastCommitTime() (time.Time, error) {
50 | return time.Now(), errors.New("no commit history in repo")
51 | }
52 |
--------------------------------------------------------------------------------
/errors.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import "github.com/gogitdb/gitdb/v2/internal/errors"
4 |
5 | var (
6 | ErrNoRecords = errors.ErrNoRecords
7 | ErrRecordNotFound = errors.ErrRecordNotFound
8 | ErrInvalidRecordID = errors.ErrInvalidRecordID
9 | ErrDBSyncFailed = errors.ErrDBSyncFailed
10 | ErrLowBattery = errors.ErrLowBattery
11 | ErrNoOnlineRemote = errors.ErrNoOnlineRemote
12 | ErrAccessDenied = errors.ErrAccessDenied
13 | ErrInvalidDataset = errors.ErrInvalidDataset
14 | )
15 |
16 | type ResolvableError interface {
17 | Resolution() string
18 | }
19 |
20 | type Error struct {
21 | err error
22 | resolution string
23 | }
24 |
25 | func (e Error) Error() string {
26 | return e.err.Error()
27 | }
28 |
29 | func (e Error) Resolution() string {
30 | return e.resolution
31 | }
32 |
33 | func ErrorWithResolution(e error, resolution string) Error {
34 | return Error{err: e, resolution: resolution}
35 | }
36 |
--------------------------------------------------------------------------------
/event.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "fmt"
5 | "github.com/bouggo/log"
6 | "github.com/distatus/battery"
7 | )
8 |
9 | type eventType string
10 |
11 | var (
12 | w eventType = "write" //write
13 | wBefore eventType = "writeBefore" //writeBefore
14 | d eventType = "delete" //delete
15 | r eventType = "read" //read
16 | )
17 |
18 | type dbEvent struct {
19 | Type eventType
20 | Dataset string
21 | Description string
22 | Commit bool
23 | }
24 |
25 | func newWriteEvent(description string, dataset string, commit bool) *dbEvent {
26 | return &dbEvent{Type: w, Description: description, Dataset: dataset, Commit: commit}
27 | }
28 |
29 | func newWriteBeforeEvent(description string, dataset string) *dbEvent {
30 | return &dbEvent{Type: wBefore, Description: description, Dataset: dataset}
31 | }
32 |
33 | func newReadEvent(description string, dataset string) *dbEvent {
34 | return &dbEvent{Type: r, Description: description, Dataset: dataset}
35 | }
36 |
37 | func newDeleteEvent(description string, dataset string, commit bool) *dbEvent {
38 | return &dbEvent{Type: w, Description: description, Dataset: dataset, Commit: commit}
39 | }
40 |
41 | func (g *gitdb) startEventLoop() {
42 | go func(g *gitdb) {
43 | log.Test("starting event loop")
44 |
45 | for {
46 | select {
47 | case <-g.shutdown:
48 | log.Info("event shutdown")
49 | log.Test("shutting down event loop")
50 | return
51 | case e := <-g.events:
52 | switch e.Type {
53 | case w, d:
54 | if e.Commit {
55 | if err := g.driver.commit(e.Dataset, e.Description, g.config.User); err != nil {
56 | log.Error(err.Error())
57 | }
58 | log.Test("handled write event for " + e.Description)
59 | }
60 | g.commit.Done()
61 | default:
62 | log.Test("No handler found for " + string(e.Type) + " event")
63 | }
64 | }
65 | }
66 | }(g)
67 |
68 | }
69 |
70 | func hasSufficientBatteryPower(threshold float64) bool {
71 | batt, err := battery.Get(0)
72 | if err != nil || batt.State == battery.Charging {
73 | //device is probably running on direct power
74 | return true
75 | }
76 |
77 | percentageCharge := batt.Current / batt.Full * 100
78 |
79 | log.Info(fmt.Sprintf("Battery Level: %6.2f%%", percentageCharge))
80 |
81 | //return true if battery life is above threshold
82 | return percentageCharge >= threshold
83 | }
84 |
--------------------------------------------------------------------------------
/example/booking/collection.go:
--------------------------------------------------------------------------------
1 | package booking
2 |
3 | //implement sort interface to allow us sort an array of files i.e []os.FileInfo
4 | // type collection []*Model
5 |
6 | // func (c collection) Len() int {
7 | // return len(c)
8 | // }
9 | // func (c collection) Less(i, j int) bool {
10 | // return c[i].CreatedAt.Before(c[j].CreatedAt)
11 | // }
12 | // func (c collection) Swap(i, j int) {
13 | // c[i], c[j] = c[j], c[i]
14 | // }
15 |
16 | // type Collection struct {
17 | // SortFunc func(i int, j int) bool
18 | // Data []*db.Model
19 | // }
20 |
21 | // func NewCollection(data []*db.Model) *Collection {
22 | // return &Collection{Data: data}
23 | // }
24 |
25 | // func (c *Collection) Len() int {
26 | // return len(c.Data)
27 | // }
28 | // func (c *Collection) Less(i, j int) bool {
29 | // return c.SortFunc(i, j)
30 | // }
31 | // func (c *Collection) Swap(i, j int) {
32 | // c.Data[i], c.Data[j] = c.Data[j], c.Data[i]
33 | // }
34 |
35 | // func (c *Collection) SortByDate(i, j int) bool {
36 | // return c.Data[i].CreatedAt.Before(c.Data[j].CreatedAt)
37 | // }
38 |
39 | // func (c *Collection) SortByName(i, j int) bool {
40 | // return c.Data[i].Name.Before(c.Data[j].CreatedAt)
41 | // }
42 |
43 | // c := NewCollection(a)
44 | // c.SortFunc = c.SortByDate
45 |
46 | // sort(c)
47 |
--------------------------------------------------------------------------------
/example/booking/enums.go:
--------------------------------------------------------------------------------
1 | package booking
2 |
3 | type Status string
4 |
5 | const (
6 | Cancelled Status = "cancelled"
7 | CheckedOut Status = "checkedout"
8 | CheckedIn Status = "checkedin"
9 | Booked Status = "booked"
10 | )
11 |
12 | type PaymentMode string
13 |
14 | const (
15 | Daily PaymentMode = "daily"
16 | Hourly PaymentMode = "hourly"
17 | )
18 |
19 |
20 | type RoomType string
21 |
22 | const (
23 | Room RoomType = "room"
24 | Hall RoomType = "hall"
25 | )
--------------------------------------------------------------------------------
/example/booking/model.go:
--------------------------------------------------------------------------------
1 | package booking
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gogitdb/gitdb/v2"
7 | )
8 |
9 | type BookingModel struct {
10 | //extends..
11 | gitdb.TimeStampedModel
12 | Type RoomType
13 | CheckInDate time.Time
14 | CheckOutDate time.Time
15 | CheckedOutAt time.Time
16 | Guests int
17 | CardsIssued int
18 | Status Status
19 | PaymentMode PaymentMode
20 | RoomPrice float64
21 | RoomId string
22 | CustomerId string
23 | UserId string
24 | NextOfKin string
25 | Purpose string
26 | }
27 |
28 | func NewBookingModel() *BookingModel {
29 | return &BookingModel{}
30 | }
31 |
32 | func (b *BookingModel) NumberOfHours() int {
33 | return int(b.CheckOutDate.Sub(b.CheckInDate).Hours())
34 | }
35 |
36 | func (b *BookingModel) NumberOfNights() int {
37 | n := int(b.CheckOutDate.Sub(b.CheckInDate).Hours() / 24)
38 | if n <= 0 {
39 | n = 1
40 | }
41 |
42 | return n
43 | }
44 |
45 | //GetSchema example
46 | func (b *BookingModel) GetSchema() *gitdb.Schema {
47 |
48 | //Name of schema
49 | name := "Booking"
50 | //Block of schema
51 | block := b.CreatedAt.Format("200601")
52 | //Record of schema
53 | record := string(b.Type) + "_" + b.CreatedAt.Format("20060102150405")
54 |
55 | //Indexes speed up searching
56 | indexes := make(map[string]interface{})
57 | indexes["RoomId"] = b.RoomId
58 | indexes["Guests"] = b.Guests
59 | indexes["CustomerId"] = b.CustomerId
60 | indexes["CreationDate"] = b.CreatedAt.Format("2006-01-02")
61 |
62 | return gitdb.NewSchema(name, block, record, indexes)
63 | }
64 |
65 | //GetLockFileNames example
66 | //Todo add comment and date expansion function
67 | func (b *BookingModel) GetLockFileNames() []string {
68 | var names []string
69 | names = append(names, "lock_"+b.CheckInDate.Format("2006-01-02")+"_"+b.RoomId)
70 | return names
71 | }
72 |
73 | //Validate example
74 | func (b *BookingModel) Validate() error {
75 | //write validation logic here
76 | return nil
77 | }
78 |
79 | //IsLockable example
80 | func (b *BookingModel) IsLockable() bool {
81 | return false
82 | }
83 |
84 | //ShouldEncrypt example
85 | func (b *BookingModel) ShouldEncrypt() bool {
86 | return false
87 | }
88 |
--------------------------------------------------------------------------------
/example/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "errors"
8 | "log"
9 | "os"
10 |
11 | db "github.com/gogitdb/gitdb/v2"
12 | "github.com/gogitdb/gitdb/v2/example/booking"
13 | )
14 |
15 | var dbconn db.GitDb
16 | var logToFile bool
17 |
18 | func init() {
19 | cfg := &db.Config{
20 | DBPath: "./data",
21 | OnlineRemote: os.Getenv("GITDB_REPO"),
22 | SyncInterval: time.Second * 5,
23 | EncryptionKey: "XVlBzgbaiCMRAjWwhTHctcuAxhxKQFDa", //this has to be 32 bytes to select AES-256
24 | User: db.NewUser("dev", "dev@gitdb.io"),
25 | EnableUI: true,
26 | }
27 |
28 | if logToFile {
29 | runLogFile, err := os.OpenFile("./db.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
30 | if err == nil {
31 | logger := log.New(runLogFile, "GITDB: ", log.Ldate|log.Ltime|log.Lshortfile)
32 | db.SetLogger(logger)
33 | }
34 | }
35 |
36 | var err error
37 | dbconn, err = db.Open(cfg)
38 | if err != nil {
39 | fmt.Println(err)
40 | os.Exit(1)
41 | }
42 | }
43 |
44 | func main() {
45 | db.SetLogLevel(db.LogLevelInfo)
46 | defer dbconn.Close()
47 | write()
48 | // fetch()
49 | testUI()
50 | }
51 |
52 | func testUI() {
53 | //simulate a block so UI can run
54 | c := make(chan int)
55 | c <- 1
56 | }
57 |
58 | func testTransaction() {
59 | t := dbconn.StartTransaction("booking")
60 | t.AddOperation(updateRoom)
61 | t.AddOperation(lockRoom)
62 | t.AddOperation(saveBooking)
63 | t.Commit()
64 | }
65 |
66 | func updateRoom() error { println("updating room..."); return nil }
67 | func lockRoom() error { println("locking room"); return errors.New("cannot lock room") }
68 | func saveBooking() error { println("saving booking"); return nil }
69 |
70 | func testWrite() {
71 | ticker := time.NewTicker(time.Second * 4)
72 | for {
73 | select {
74 | case <-ticker.C:
75 | write()
76 | }
77 | }
78 | }
79 |
80 | func write() {
81 | //populate model
82 | bm := booking.NewBookingModel()
83 | bm.Type = booking.Room
84 | bm.CheckInDate = time.Now()
85 | bm.CheckOutDate = time.Now()
86 | bm.CustomerId = "customer_1"
87 | bm.Guests = 2
88 | bm.CardsIssued = 1
89 | bm.NextOfKin = "Kin 1"
90 | bm.Status = booking.CheckedIn
91 | bm.UserId = "user_1"
92 | bm.RoomId = "room_1"
93 |
94 | err := dbconn.Insert(bm)
95 | if err != nil {
96 | fmt.Println(err)
97 | }
98 | }
99 |
100 | func read() {
101 | b := &booking.BookingModel{}
102 | err := dbconn.Get("Booking/201802/room_201802070030", b)
103 | if err != nil {
104 | fmt.Println(err.Error())
105 | } else {
106 | fmt.Println(b.NextOfKin)
107 | }
108 | }
109 |
110 | func delete() {
111 | err := dbconn.Delete("Booking/201801/room_201801111823")
112 | if err != nil {
113 | fmt.Println(err.Error())
114 | } else {
115 | fmt.Println("Deleted")
116 | }
117 | }
118 |
119 | func search() {
120 | searchParam := &db.SearchParam{Index: "CustomerId", Value: "customer_2"}
121 | rows, err := dbconn.Search("Booking", []*db.SearchParam{searchParam}, db.SearchEquals)
122 | if err != nil {
123 | fmt.Println(err.Error())
124 | } else {
125 | for _, r := range rows {
126 | fmt.Println(r)
127 | }
128 | }
129 | }
130 |
131 | func fetch() {
132 | rows, err := dbconn.Fetch("Booking")
133 | if err != nil {
134 | fmt.Println(err.Error())
135 | } else {
136 | bookings := []*booking.BookingModel{}
137 | for _, r := range rows {
138 | b := &booking.BookingModel{}
139 | r.Hydrate(b)
140 | bookings = append(bookings, b)
141 |
142 | fmt.Println(fmt.Sprintf("%s-%s", db.ID(b), b.CreatedAt))
143 | }
144 | }
145 | }
146 |
147 | func mail() {
148 | mails := dbconn.GetMails()
149 | for _, m := range mails {
150 | fmt.Println(m.Body)
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/gogitdb/gitdb/v2
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/bouggo/log v0.0.1
7 | github.com/distatus/battery v0.10.0
8 | github.com/gorilla/mux v1.7.4
9 | github.com/valyala/fastjson v1.5.1
10 | golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79
11 | golang.org/x/sys v0.0.0-20220721230656-c6bc011c0c49 // indirect
12 | howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5 // indirect
13 | )
14 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/bouggo/log v0.0.1 h1:ki+t3NRgbcLtO3UpnzRwtWHsmB9Q/nYGlY+aM7I5AM8=
2 | github.com/bouggo/log v0.0.1/go.mod h1:3gQbYNgxubDvcQHMWOMcoCIbhw0x/Q3dCNZcLTy/bPI=
3 | github.com/distatus/battery v0.10.0 h1:YbizvmV33mqqC1fPCAEaQGV3bBhfYOfM+2XmL+mvt5o=
4 | github.com/distatus/battery v0.10.0/go.mod h1:STnSvFLX//eEpkaN7qWRxCWxrWOcssTDgnG4yqq9BRE=
5 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
6 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
7 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
8 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
9 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
10 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
11 | github.com/valyala/fastjson v1.5.1 h1:SXaQZVSwLjZOVhDEhjiCcDtnX0Feu7Z7A1+C5atpoHM=
12 | github.com/valyala/fastjson v1.5.1/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
13 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
14 | golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88=
15 | golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
16 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
17 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
18 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
19 | golang.org/x/sys v0.0.0-20190912141932-bc967efca4b8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
20 | golang.org/x/sys v0.0.0-20220721230656-c6bc011c0c49 h1:TMjZDarEwf621XDryfitp/8awEhiZNiwgphKlTMGRIg=
21 | golang.org/x/sys v0.0.0-20220721230656-c6bc011c0c49/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
22 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
23 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
24 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
25 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
26 | howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
27 | howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5 h1:AQkaJpH+/FmqRjmXZPELom5zIERYZfwTjnHpfoVMQEc=
28 | howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
29 |
--------------------------------------------------------------------------------
/index.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "encoding/json"
7 | "fmt"
8 | "io/ioutil"
9 | "os"
10 | "path/filepath"
11 | "strings"
12 |
13 | "github.com/bouggo/log"
14 | "github.com/gogitdb/gitdb/v2/internal/db"
15 | )
16 |
17 | type gdbIndex map[string]gdbIndexValue
18 | type gdbIndexCache map[string]gdbIndex
19 | type gdbSimpleIndex map[string]interface{} //recordID => value
20 | type gdbSimpleIndexCache map[string]gdbSimpleIndex //index => (recordID => value)
21 | type gdbIndexValue struct {
22 | Offset int `json:"o"`
23 | Len int `json:"l"`
24 | Value interface{} `json:"v"`
25 | }
26 |
27 | //TODO handle deletes
28 | func (g *gitdb) updateIndexes(dataBlock *db.Block) {
29 | g.indexMu.Lock()
30 | defer g.indexMu.Unlock()
31 |
32 | g.indexUpdated = true
33 | dataset := dataBlock.Dataset().Name()
34 | indexPath := g.indexPath(dataset)
35 |
36 | log.Info("updating in-memory index: " + dataset)
37 | //get line position of each record in the block
38 | //p := extractPositions(dataBlock)
39 |
40 | model := g.registry[dataset]
41 | if model == nil {
42 | if g.config.Factory != nil {
43 | model = g.config.Factory(dataset)
44 | }
45 | }
46 |
47 | if model == nil {
48 | log.Error(fmt.Sprintf("model not found in registry or factory: %s", dataset))
49 | return
50 | }
51 |
52 | var indexes map[string]interface{}
53 | for _, record := range dataBlock.Records() {
54 | if err := record.Hydrate(model); err != nil {
55 | log.Error(fmt.Sprintf("record.Hydrate failed: %s %s", record.ID(), err))
56 | }
57 | indexes = model.GetSchema().indexes
58 |
59 | //append index for id
60 | recordID := record.ID()
61 | indexes["id"] = recordID
62 |
63 | for name, value := range indexes {
64 | indexFile := filepath.Join(indexPath, name+".json")
65 | if _, ok := g.indexCache[indexFile]; !ok {
66 | g.indexCache[indexFile] = g.readIndex(indexFile)
67 | }
68 | //g.indexCache[indexFile][recordID] = gdbIndexValue{
69 | // Offset: p[recordID][0],
70 | // Len: p[recordID][1],
71 | // Value: value,
72 | //}
73 | g.indexCache[indexFile][recordID] = value
74 | }
75 | }
76 | }
77 |
78 | func (g *gitdb) flushIndex() error {
79 | g.indexMu.Lock()
80 | defer g.indexMu.Unlock()
81 |
82 | if g.indexUpdated {
83 | log.Test("flushing index")
84 | for indexFile, data := range g.indexCache {
85 |
86 | indexPath := filepath.Dir(indexFile)
87 | if _, err := os.Stat(indexPath); err != nil {
88 | if err := os.MkdirAll(indexPath, 0755); err != nil {
89 | log.Error("Failed to write to index: " + indexFile)
90 | return err
91 | }
92 | }
93 |
94 | // indexBytes, err := json.MarshalIndent(data, "", "\t")
95 | indexBytes, err := json.Marshal(data)
96 | if err != nil {
97 | log.Error("Failed to write to index [" + indexFile + "]: " + err.Error())
98 | return err
99 | }
100 |
101 | if err := ioutil.WriteFile(indexFile, indexBytes, 0744); err != nil {
102 | log.Error("Failed to write to index: " + indexFile)
103 | return err
104 | }
105 | }
106 | g.indexUpdated = false
107 | }
108 |
109 | return nil
110 | }
111 |
112 | func (g *gitdb) readIndex(indexFile string) gdbSimpleIndex {
113 | rMap := make(gdbSimpleIndex)
114 | if _, err := os.Stat(indexFile); err == nil {
115 | data, err := ioutil.ReadFile(indexFile)
116 | if err == nil {
117 | err = json.Unmarshal(data, &rMap)
118 | }
119 |
120 | if err != nil {
121 | log.Error(err.Error())
122 | }
123 | }
124 | return rMap
125 | }
126 |
127 | func (g *gitdb) buildIndexSmart(changedFiles []string) {
128 | for _, blockFile := range changedFiles {
129 | log.Info("Building index for block: " + blockFile)
130 | block := db.LoadBlock(filepath.Join(g.dbDir(), blockFile), g.config.EncryptionKey)
131 | g.updateIndexes(block)
132 | }
133 | log.Info("Building index complete")
134 | }
135 |
136 | func (g *gitdb) buildIndexTargeted(target string) {
137 | ds := db.LoadDataset(filepath.Join(g.dbDir(), target), g.config.EncryptionKey)
138 | for _, block := range ds.Blocks() {
139 | g.updateIndexes(block)
140 | }
141 | }
142 |
143 | func (g *gitdb) buildIndexFull() {
144 | datasets := db.LoadDatasets(g.dbDir(), g.config.EncryptionKey)
145 | for _, ds := range datasets {
146 | g.buildIndexTargeted(ds.Name())
147 | }
148 | if err := g.flushIndex(); err != nil {
149 | log.Error("gitDB: flushIndex failed: " + err.Error())
150 | }
151 | }
152 |
153 | //extractPositions returns the position of all records in a block
154 | //as they would appear in the physical block file
155 | //a block can contain records from multiple physical block files
156 | //especially when *gitdb.doFetch is called so proceed with caution
157 | func extractPositions(b *db.Block) map[string][]int {
158 | var positions = map[string][]int{}
159 |
160 | data, err := json.MarshalIndent(b, "", "\t")
161 | if err != nil {
162 | log.Error(err.Error())
163 | }
164 |
165 | offset := 3
166 | scanner := bufio.NewScanner(bytes.NewReader(data))
167 | for scanner.Scan() {
168 | line := scanner.Text()
169 | if strings.TrimSpace(line) != "{" && strings.TrimSpace(line) != "}" {
170 | length := len(line)
171 |
172 | record := strings.SplitN(line, ":", 2)
173 | if len(record) != 2 {
174 | continue
175 | }
176 |
177 | recordID := strings.TrimSpace(record[0])
178 | recordID = recordID[1 : len(recordID)-1]
179 | positions[recordID] = []int{offset, length}
180 |
181 | //account for newline
182 | offset += length + 1
183 | }
184 | }
185 |
186 | if len(positions) < 1 {
187 | panic("no positions extracted from: " + b.Path())
188 | }
189 |
190 | return positions
191 | }
192 |
--------------------------------------------------------------------------------
/init.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io/ioutil"
7 | "os"
8 | "sync"
9 |
10 | "github.com/bouggo/log"
11 | )
12 |
13 | var mu sync.Mutex
14 | var conns map[string]GitDb
15 |
16 | // Open opens a connection to GitDB
17 | func Open(config *Config) (GitDb, error) {
18 | cfg := *config
19 |
20 | if err := cfg.Validate(); err != nil {
21 | return nil, err
22 | }
23 |
24 | if conns == nil {
25 | conns = make(map[string]GitDb)
26 | }
27 |
28 | if cfg.Mock {
29 | conn := newMockConnection()
30 | conn.configure(cfg)
31 | conns[cfg.ConnectionName] = conn
32 | return conn, nil
33 | }
34 |
35 | conn := newConnection()
36 | conn.configure(cfg)
37 |
38 | err := conn.boot()
39 | logMsg := "Db booted fine"
40 | if err != nil {
41 | logMsg = fmt.Sprintf("Db booted with errors - %s", err)
42 | }
43 |
44 | log.Info(logMsg)
45 |
46 | if err != nil {
47 | if errors.Is(err, ErrAccessDenied) {
48 | fb, readErr := ioutil.ReadFile(conn.publicKeyFilePath())
49 | if readErr != nil {
50 | return nil, readErr
51 | }
52 |
53 | // inform users to ask admin to add their public key to repo
54 | resolution := "Contact your database admin to add your public key to git server\n"
55 | resolution += "Public key: " + fmt.Sprintf("%s", fb)
56 |
57 | return nil, ErrorWithResolution(err, resolution)
58 | }
59 |
60 | return nil, err
61 | }
62 |
63 | // if boot() returned an error do not start event loop
64 | if !conn.loopStarted {
65 | conn.startEventLoop()
66 | if cfg.SyncInterval > 0 {
67 | conn.startSyncClock()
68 | }
69 | if cfg.EnableUI {
70 | conn.startUI()
71 | }
72 | conn.loopStarted = true
73 | }
74 |
75 | conns[cfg.ConnectionName] = conn
76 | return conn, nil
77 | }
78 |
79 | // Conn returns the last connection started by Open(*Config)
80 | // if you opened more than one connection use GetConn(name) instead
81 | func Conn() GitDb {
82 | if len(conns) > 1 {
83 | panic("Multiple gitdb connections found. Use GetConn function instead")
84 | }
85 |
86 | if len(conns) == 0 {
87 | panic("No open gitdb connections found")
88 | }
89 |
90 | var connName string
91 | for k := range conns {
92 | connName = k
93 | break
94 | }
95 |
96 | if _, ok := conns[connName]; !ok {
97 | panic("No gitdb connection found - " + connName)
98 | }
99 |
100 | return conns[connName]
101 | }
102 |
103 | // GetConn returns a specific gitdb connection by name
104 | func GetConn(name string) GitDb {
105 | if _, ok := conns[name]; !ok {
106 | panic("No gitdb connection found")
107 | }
108 |
109 | return conns[name]
110 | }
111 |
112 | func (g *gitdb) boot() error {
113 | log.Info("Booting up db using " + g.driver.name() + " driver")
114 |
115 | if err := g.driver.setup(g); err != nil {
116 | return err
117 | }
118 |
119 | // rebuild index if we have to
120 | if _, err := os.Stat(g.indexDir()); err != nil {
121 | // no index directory found so we need to re-index the whole db
122 | go g.buildIndexFull()
123 | }
124 |
125 | return nil
126 | }
127 |
--------------------------------------------------------------------------------
/init_test.go:
--------------------------------------------------------------------------------
1 | package gitdb_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/gogitdb/gitdb/v2"
7 | )
8 |
9 | func TestConn(t *testing.T) {
10 | setup(t, nil)
11 | defer testDb.Close()
12 | got := gitdb.Conn()
13 | if got != testDb {
14 | t.Errorf("connection don't match")
15 | }
16 | }
17 |
18 | func TestGetConn(t *testing.T) {
19 | setup(t, nil)
20 | defer testDb.Close()
21 | got := gitdb.GetConn("default")
22 | if got != testDb {
23 | t.Errorf("connection don't match")
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/internal/crypto/security.go:
--------------------------------------------------------------------------------
1 | package crypto
2 |
3 | import (
4 | "crypto/aes"
5 | "crypto/cipher"
6 | "crypto/rand"
7 | "encoding/base64"
8 | "io"
9 | )
10 |
11 | //Encrypt message with key
12 | func Encrypt(key string, message string) string {
13 | plainText := []byte(message)
14 |
15 | block, err := aes.NewCipher([]byte(key))
16 | if err != nil {
17 | println(err.Error())
18 | return ""
19 | }
20 |
21 | //IV needs to be unique, but doesn't have to be secure.
22 | //It's common to put it at the beginning of the ciphertext.
23 | cipherText := make([]byte, aes.BlockSize+len(plainText))
24 | iv := cipherText[:aes.BlockSize]
25 | if _, err = io.ReadFull(rand.Reader, iv); err != nil {
26 | println(err.Error())
27 | return ""
28 | }
29 |
30 | stream := cipher.NewCFBEncrypter(block, iv)
31 | stream.XORKeyStream(cipherText[aes.BlockSize:], plainText)
32 |
33 | //returns to base64 encoded string
34 | encmess := base64.URLEncoding.EncodeToString(cipherText)
35 | return encmess
36 | }
37 |
38 | //Decrypt message with key
39 | func Decrypt(key string, secureMessage string) string {
40 | cipherText, err := base64.URLEncoding.DecodeString(secureMessage)
41 | if err != nil {
42 | return ""
43 | }
44 |
45 | block, err := aes.NewCipher([]byte(key))
46 | if err != nil {
47 | return ""
48 | }
49 |
50 | if len(cipherText) < aes.BlockSize {
51 | return ""
52 | }
53 |
54 | iv := cipherText[:aes.BlockSize]
55 | cipherText = cipherText[aes.BlockSize:]
56 |
57 | stream := cipher.NewCFBDecrypter(block, iv)
58 | // XORKeyStream can work in-place if the two arguments are the same.
59 | stream.XORKeyStream(cipherText, cipherText)
60 |
61 | decodedmess := string(cipherText)
62 | return decodedmess
63 | }
64 |
--------------------------------------------------------------------------------
/internal/db/block.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "github.com/gogitdb/gitdb/v2/internal/errors"
7 | "io/ioutil"
8 | "os"
9 | "path/filepath"
10 | "sort"
11 |
12 | "github.com/bouggo/log"
13 | "github.com/gogitdb/gitdb/v2/internal/digital"
14 | )
15 |
16 | //Block represents a block file
17 | type Block struct {
18 | dataset *Dataset
19 | path string
20 | key string
21 | size int64
22 | badRecords []string
23 | records map[string]*Record
24 | }
25 |
26 | //EmptyBlock is used for hydration
27 | type EmptyBlock struct {
28 | Block
29 | }
30 |
31 | //HydrateByPositions should be called on EmptyBlock
32 | //pos must be []int{offset, position}
33 | func (b *EmptyBlock) HydrateByPositions(blockFilePath string, positions ...[]int) error {
34 | fd, err := os.Open(blockFilePath)
35 | if err != nil {
36 | return err
37 | }
38 | defer fd.Close()
39 |
40 | blockJSON := []byte("{")
41 | for i, pos := range positions {
42 |
43 | fd.Seek(int64(pos[0]), 0)
44 | line := make([]byte, pos[1])
45 | fd.Read(line)
46 |
47 | line = bytes.TrimSpace(line)
48 | ln := len(line) - 1
49 | //are we at the end of seek
50 | if i < len(positions)-1 {
51 | //ensure line ends with a comma
52 | if line[ln] != ',' {
53 | line = append(line, ',')
54 |
55 | }
56 | } else {
57 | //make sure last line has no comma
58 | if line[ln] == ',' {
59 | line = line[0:ln]
60 | }
61 | }
62 | line = append(line, "\n"...)
63 | blockJSON = append(blockJSON, line...)
64 | }
65 | blockJSON = append(blockJSON, '}')
66 | return json.Unmarshal(blockJSON, b)
67 | }
68 |
69 | //Hydrate should be called on EmptyBlock
70 | func (b *EmptyBlock) Hydrate(blockFilePath string) error {
71 | data, err := ioutil.ReadFile(blockFilePath)
72 | if err != nil {
73 | return err
74 | }
75 |
76 | if err := json.Unmarshal(data, b); err != nil {
77 | return err //errBadBlock
78 | }
79 |
80 | return err
81 | }
82 |
83 | //Dataset returns the dataset *Block belongs to
84 | func (b *Block) Dataset() *Dataset {
85 | return b.dataset
86 | }
87 |
88 | //HumanSize returns human readable size of a block
89 | func (b *Block) HumanSize() string {
90 | return digital.FormatBytes(uint64(b.size))
91 | }
92 |
93 | //RecordCount returns the number of records in a block
94 | func (b *Block) RecordCount() int {
95 | return len(b.records)
96 | }
97 |
98 | //BadRecords returns all bad records found in a block
99 | func (b *Block) BadRecords() []string {
100 | return b.badRecords
101 | }
102 |
103 | //Path returns path to block file
104 | func (b *Block) Path() string {
105 | return b.path
106 | }
107 |
108 | //NewEmptyBlock should be used to store records from multiple blocks
109 | func NewEmptyBlock(key string) *EmptyBlock {
110 | return &EmptyBlock{Block{
111 | key: key,
112 | records: map[string]*Record{},
113 | badRecords: []string{},
114 | }}
115 | }
116 |
117 | //LoadBlock loads a block at a particular path
118 | func LoadBlock(blockFilePath, key string) *Block {
119 | block := &Block{
120 | path: blockFilePath,
121 | key: key,
122 | records: map[string]*Record{},
123 | badRecords: []string{},
124 | //TODO figure out a neat way to inject key
125 | dataset: &Dataset{path: filepath.Dir(blockFilePath), key: key},
126 | }
127 |
128 | if err := block.load(); err != nil {
129 | log.Error(err.Error())
130 | block.dataset.badBlocks = append(block.dataset.badBlocks, blockFilePath)
131 | }
132 |
133 | return block
134 | }
135 |
136 | //MarshalJSON implements json.MarshalJSON
137 | func (b *Block) MarshalJSON() ([]byte, error) {
138 | raw := map[string]string{}
139 | for k, v := range b.records {
140 | raw[k] = v.data
141 | }
142 |
143 | return json.Marshal(raw)
144 | }
145 |
146 | //UnmarshalJSON implements json.UnmarshalJSON
147 | func (b *Block) UnmarshalJSON(data []byte) error {
148 | var raw map[string]string
149 | if err := json.Unmarshal(data, &raw); err != nil {
150 | return err
151 | }
152 |
153 | //populate recs
154 | for k, v := range raw {
155 | r := newRecord(k, v)
156 | b.records[k] = r
157 | }
158 |
159 | return nil
160 | }
161 |
162 | func (b *Block) load() error {
163 | blockFile := filepath.Join(b.path)
164 | log.Info("Reading block: " + blockFile)
165 | data, err := ioutil.ReadFile(blockFile)
166 | if err != nil {
167 | return err
168 | }
169 |
170 | b.size = int64(len(data))
171 | return json.Unmarshal(data, b)
172 | }
173 |
174 | //Record returns record in specifed index i
175 | func (b *Block) Record(i int) *Record {
176 | records := b.Records()
177 | record := records[i]
178 | return record
179 | }
180 |
181 | //Add a record to Block
182 | func (b *Block) Add(recordID, value string) {
183 | b.records[recordID] = newRecord(recordID, value)
184 | }
185 |
186 | //Get a record by key from a Block
187 | func (b *Block) Get(key string) (*Record, error) {
188 | if _, ok := b.records[key]; ok {
189 | b.records[key].key = b.key
190 | return b.records[key], nil
191 | }
192 |
193 | return nil, errors.ErrRecordNotFound
194 | }
195 |
196 | //Delete a record by key from a Block
197 | func (b *Block) Delete(key string) error {
198 | if _, ok := b.records[key]; ok {
199 | delete(b.records, key)
200 | return nil
201 | }
202 |
203 | return errors.ErrRecordNotFound
204 | }
205 |
206 | //Len returns length of block
207 | func (b *Block) Len() int {
208 | return len(b.records)
209 | }
210 |
211 | //Records returns decrypted slice of all Records in a Block
212 | //sorted in asc order of id
213 | func (b *Block) Records() []*Record {
214 | var records []*Record
215 | for _, v := range b.records {
216 | v.decrypt(b.key)
217 | records = append(records, v)
218 | }
219 |
220 | sort.Sort(collection(records))
221 |
222 | return records
223 | }
224 |
225 | func (b *Block) Filter(recordIDs map[string]string) {
226 | records := make(map[string]*Record, len(recordIDs))
227 | for recordID, value := range b.records {
228 | if _, ok := recordIDs[recordID]; ok {
229 | records[recordID] = value
230 | }
231 |
232 | }
233 | b.records = records
234 | }
235 |
--------------------------------------------------------------------------------
/internal/db/dataset.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "io/ioutil"
5 | "path"
6 | "path/filepath"
7 | "strings"
8 | "time"
9 |
10 | "github.com/bouggo/log"
11 | "github.com/gogitdb/gitdb/v2/internal/digital"
12 | )
13 |
14 | //Dataset represent a collection of blocks
15 | type Dataset struct {
16 | path string
17 | blocks []*Block
18 | badBlocks []string
19 | badRecords []string
20 | lastModified time.Time
21 |
22 | key string
23 | }
24 |
25 | //LoadDataset loads the dataset at path
26 | func LoadDataset(datasetPath, key string) *Dataset {
27 | ds := &Dataset{
28 | path: datasetPath,
29 | key: key,
30 | }
31 | ds.loadBlocks()
32 |
33 | return ds
34 | }
35 |
36 | //LoadDatasets loads all datasets in given gitdb path
37 | func LoadDatasets(dbPath, key string) []*Dataset {
38 | var datasets []*Dataset
39 |
40 | dirs, err := ioutil.ReadDir(dbPath)
41 | if err != nil {
42 | log.Error(err.Error())
43 | return datasets
44 | }
45 |
46 | for _, dir := range dirs {
47 | if !strings.HasPrefix(dir.Name(), ".") && dir.IsDir() {
48 | ds := &Dataset{
49 | path: filepath.Join(dbPath, dir.Name()),
50 | lastModified: dir.ModTime(),
51 | key: key,
52 | }
53 |
54 | datasets = append(datasets, ds)
55 | }
56 | }
57 | return datasets
58 | }
59 |
60 | //Name returns name of dataset
61 | func (d *Dataset) Name() string {
62 | return filepath.Base(d.path)
63 | }
64 |
65 | //Path returns path to dataset
66 | func (d *Dataset) Path() string {
67 | return d.path
68 | }
69 |
70 | //Size returns the total size of all blocks in a DataSet
71 | func (d *Dataset) Size() int64 {
72 | size := int64(0)
73 | for _, block := range d.blocks {
74 | size += block.size
75 | }
76 |
77 | return size
78 | }
79 |
80 | //HumanSize returns human friendly size of a DataSet
81 | func (d *Dataset) HumanSize() string {
82 | return digital.FormatBytes(uint64(d.Size()))
83 | }
84 |
85 | //BlockCount returns the number of blocks in a DataSet
86 | func (d *Dataset) BlockCount() int {
87 | if len(d.blocks) == 0 {
88 | d.loadBlocks()
89 | }
90 | return len(d.blocks)
91 | }
92 |
93 | //RecordCount returns the number of records in a DataSet
94 | func (d *Dataset) RecordCount() int {
95 | count := 0
96 | if d.BlockCount() > 0 {
97 | for _, block := range d.blocks {
98 | count += block.RecordCount()
99 | }
100 | }
101 |
102 | return count
103 | }
104 |
105 | //BadBlocksCount returns the number of bad blocks in a DataSet
106 | func (d *Dataset) BadBlocksCount() int {
107 | if len(d.badBlocks) == 0 {
108 | d.RecordCount() //hack to get records loaded so errors can be populated in dataset
109 | }
110 |
111 | return len(d.badBlocks)
112 | }
113 |
114 | //BadRecordsCount returns the number of bad records in a DataSet
115 | func (d *Dataset) BadRecordsCount() int {
116 | if len(d.badRecords) == 0 {
117 | d.RecordCount() //hack to get records loaded so errors can be populated in dataset
118 | }
119 |
120 | return len(d.badRecords)
121 | }
122 |
123 | //Block returns block at index i of a DataSet
124 | func (d *Dataset) Block(i int) *Block {
125 | if len(d.blocks) == 0 {
126 | //load blocks so errors can be populated in dataset
127 | d.loadBlocks()
128 | }
129 |
130 | if i <= len(d.blocks)-1 {
131 | return d.blocks[i]
132 | }
133 |
134 | return nil
135 | }
136 |
137 | //Blocks returns all the Blocks in a dataset
138 | func (d *Dataset) Blocks() []*Block {
139 | return d.blocks
140 | }
141 |
142 | //BadBlocks returns all the bad blocks in a dataset
143 | func (d *Dataset) BadBlocks() []string {
144 | return d.badBlocks
145 | }
146 |
147 | //BadRecords returns all the bad records in a dataset
148 | func (d *Dataset) BadRecords() []string {
149 | return d.badRecords
150 | }
151 |
152 | //LastModifiedDate returns the last modification time of a DataSet
153 | func (d *Dataset) LastModifiedDate() string {
154 | return d.lastModified.Format("02 Jan 2006 15:04:05")
155 | }
156 |
157 | //loadBlocks reads all blocks in a Dataset into memory
158 | func (d *Dataset) loadBlocks() {
159 | blks, err := ioutil.ReadDir(d.path)
160 | if err != nil {
161 | log.Error(err.Error())
162 | }
163 |
164 | for _, blk := range blks {
165 | if !blk.IsDir() && strings.HasSuffix(blk.Name(), ".json") {
166 | b := LoadBlock(filepath.Join(d.path, blk.Name()), d.key)
167 | d.blocks = append(d.blocks, b)
168 | }
169 | }
170 | }
171 |
172 | //Indexes returns the indexes set on a DataSet
173 | func (d *Dataset) Indexes() []string {
174 | //grab indexes
175 | var indexes []string
176 |
177 | indexFiles, err := ioutil.ReadDir(filepath.Join(path.Dir(d.path), ".gitdb/index/", d.Name()))
178 | if err != nil {
179 | return indexes
180 | }
181 |
182 | for _, indexFile := range indexFiles {
183 | indexes = append(indexes, strings.TrimSuffix(indexFile.Name(), ".json"))
184 | }
185 |
186 | return indexes
187 | }
188 |
--------------------------------------------------------------------------------
/internal/db/record.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 |
8 | "github.com/bouggo/log"
9 | "github.com/gogitdb/gitdb/v2/internal/crypto"
10 | "github.com/valyala/fastjson"
11 | )
12 |
13 | //Record represents a model stored in gitdb
14 | type Record struct {
15 | id string
16 | data string
17 | key string
18 |
19 | p fastjson.Parser
20 | decrypted bool
21 | }
22 |
23 | //newRecord constructs a Record
24 | func newRecord(id, data string) *Record {
25 | return &Record{id: id, data: data}
26 | }
27 |
28 | //ID returns record id
29 | func (r *Record) ID() string {
30 | return r.id
31 | }
32 |
33 | //Data returns record data unmodified
34 | func (r *Record) Data() string {
35 | return r.data
36 | }
37 |
38 | //Hydrate populates given interfacce with underlying record data
39 | func (r *Record) Hydrate(model interface{}) error {
40 | r.decrypt(r.key)
41 | version := r.Version()
42 | switch version {
43 | case "v1":
44 | if err := json.Unmarshal([]byte(r.data), model); err != nil {
45 | return err
46 | }
47 | return nil
48 | case "v2": //TODO Optimize Unmarshall-Marshall technique
49 | v, err := r.p.Parse(r.data)
50 | if err != nil {
51 | return err
52 | }
53 |
54 | obj := v.GetObject("Data")
55 | buf := make([]byte, obj.Len())
56 | buf = obj.MarshalTo(buf)
57 | buf = bytes.Trim(buf, "\x00")
58 |
59 | // fmt.Printf("%s\n", oh)
60 | return json.Unmarshal(buf, model)
61 | default:
62 | return fmt.Errorf("Unable to hydrate version : %s", version)
63 | }
64 | }
65 |
66 | func (r *Record) decrypt(key string) {
67 | if len(key) > 0 && !r.decrypted {
68 | log.Test("decrypting with: " + key)
69 | dec := crypto.Decrypt(key, r.data)
70 | if len(dec) > 0 {
71 | r.data = dec
72 | }
73 | r.decrypted = true
74 | }
75 | }
76 |
77 | //JSON returns data decrypted and indented
78 | func (r *Record) JSON() string {
79 | var buf bytes.Buffer
80 | r.decrypt(r.key)
81 | if err := json.Indent(&buf, []byte(r.data), "", "\t"); err != nil {
82 | log.Error(err.Error())
83 | }
84 |
85 | return buf.String()
86 | }
87 |
88 | //Version returns the version of the record
89 | func (r *Record) Version() string {
90 | v, err := r.p.Parse(r.data)
91 | if err != nil {
92 | return "v1"
93 | }
94 |
95 | versionBytes := v.GetStringBytes("Version")
96 | version := string(versionBytes)
97 | if len(version) == 0 {
98 | version = "v1"
99 | }
100 |
101 | return version
102 | }
103 |
104 | //ConvertModel converts a Model to a record
105 | func ConvertModel(id string, m interface{}) *Record {
106 | b, _ := json.Marshal(m)
107 | return newRecord(id, string(b))
108 | }
109 |
110 | //collection represents a sortable slice of Records
111 | type collection []*Record
112 |
113 | func (c collection) Len() int {
114 | return len(c)
115 | }
116 | func (c collection) Less(i, j int) bool {
117 | return c[i].id < c[j].id
118 | }
119 | func (c collection) Swap(i, j int) {
120 | c[i], c[j] = c[j], c[i]
121 | }
122 |
--------------------------------------------------------------------------------
/internal/digital/size.go:
--------------------------------------------------------------------------------
1 | package digital
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | const (
9 | sizeByte = 1.0 << (10 * iota)
10 | sizeKb
11 | sizeMb
12 | sizeGb
13 | sizeTb
14 | )
15 |
16 | func FormatBytes(bytes uint64) string {
17 | unit := ""
18 | value := float32(bytes)
19 |
20 | switch {
21 | case bytes >= sizeTb:
22 | unit = "TB"
23 | value = value / sizeTb
24 | case bytes >= sizeGb:
25 | unit = "GB"
26 | value = value / sizeGb
27 | case bytes >= sizeMb:
28 | unit = "MB"
29 | value = value / sizeMb
30 | case bytes >= sizeKb:
31 | unit = "KB"
32 | value = value / sizeKb
33 | case bytes >= sizeByte:
34 | unit = "B"
35 | case bytes == 0:
36 | return "0"
37 | }
38 |
39 | stringValue := fmt.Sprintf("%.1f", value)
40 | stringValue = strings.TrimSuffix(stringValue, ".0")
41 | return fmt.Sprintf("%s%s", stringValue, unit)
42 | }
43 |
--------------------------------------------------------------------------------
/internal/errors/errors.go:
--------------------------------------------------------------------------------
1 | package errors
2 |
3 | import "errors"
4 |
5 | var (
6 | //internal errors
7 | errDB = errors.New("gitDB: database error")
8 | errBadBlock = errors.New("gitDB: Bad block error - invalid json")
9 | errBadRecord = errors.New("gitDB: Bad record error")
10 | errConnectionClosed = errors.New("gitDB: connection is closed")
11 | errConnectionInvalid = errors.New("gitDB: connection is not valid. use gitdb.Start to construct a valid connection")
12 |
13 | //external errors
14 | ErrNoRecords = errors.New("gitDB: no records found")
15 | ErrRecordNotFound = errors.New("gitDB: record not found")
16 | ErrInvalidRecordID = errors.New("gitDB: invalid record id")
17 | ErrDBSyncFailed = errors.New("gitDB: Database sync failed")
18 | ErrLowBattery = errors.New("gitDB: Insufficient battery power. Syncing disabled")
19 | ErrNoOnlineRemote = errors.New("gitDB: Online remote is not set. Syncing disabled")
20 | ErrAccessDenied = errors.New("gitDB: Access was denied to online repository")
21 | ErrInvalidDataset = errors.New("gitDB: invalid dataset. Dataset not in registry")
22 | )
23 |
--------------------------------------------------------------------------------
/lock.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "errors"
5 | "io/ioutil"
6 | "os"
7 | "path/filepath"
8 | "strings"
9 |
10 | "github.com/bouggo/log"
11 | )
12 |
13 | func (g *gitdb) Lock(mo Model) error {
14 |
15 | m := wrap(mo)
16 | if _, ok := mo.(LockableModel); !ok {
17 | return errors.New("Model is not lockable")
18 | }
19 |
20 | var lockFilesWritten []string
21 |
22 | fullPath := g.lockDir(m)
23 | if _, err := os.Stat(fullPath); err != nil {
24 | err := os.MkdirAll(fullPath, 0755)
25 | if err != nil {
26 | return err
27 | }
28 | }
29 |
30 | lockFiles := mo.(LockableModel).GetLockFileNames()
31 | for _, file := range lockFiles {
32 | lockFile := filepath.Join(fullPath, file+".lock")
33 | g.events <- newWriteBeforeEvent("...", lockFile)
34 |
35 | //when locking a model, lockfile should not exist
36 | if _, err := os.Stat(lockFile); err == nil {
37 | if derr := g.deleteLockFiles(lockFilesWritten); derr != nil {
38 | log.Error(derr.Error())
39 | }
40 | return errors.New("Lock file already exist: " + lockFile)
41 | }
42 |
43 | err := ioutil.WriteFile(lockFile, []byte(""), 0644)
44 | if err != nil {
45 | if derr := g.deleteLockFiles(lockFilesWritten); derr != nil {
46 | log.Error(derr.Error())
47 | }
48 | return errors.New("Failed to write lock " + lockFile + ": " + err.Error())
49 | }
50 |
51 | lockFilesWritten = append(lockFilesWritten, lockFile)
52 | }
53 |
54 | g.commit.Add(1)
55 | commitMsg := "Created Lock Files for: " + ID(m)
56 | g.events <- newWriteEvent(commitMsg, fullPath, g.autoCommit)
57 |
58 | //block here until write has been committed
59 | g.waitForCommit()
60 | return nil
61 | }
62 |
63 | func (g *gitdb) Unlock(mo Model) error {
64 |
65 | m := wrap(mo)
66 | if _, ok := mo.(LockableModel); !ok {
67 | return errors.New("Model is not lockable")
68 | }
69 |
70 | fullPath := g.lockDir(m)
71 |
72 | lockFiles := mo.(LockableModel).GetLockFileNames()
73 | for _, file := range lockFiles {
74 | lockFile := filepath.Join(fullPath, file+".lock")
75 |
76 | if _, err := os.Stat(lockFile); err == nil {
77 | //log.PutInfo("Removing " + lockFile)
78 | err := os.Remove(lockFile)
79 | if err != nil {
80 | return errors.New("Could not delete lock file: " + lockFile)
81 | }
82 | }
83 | }
84 |
85 | g.commit.Add(1)
86 | commitMsg := "Removing Lock Files for: " + ID(m)
87 | g.events <- newWriteEvent(commitMsg, fullPath, g.autoCommit)
88 |
89 | //block here until write has been committed
90 | g.waitForCommit()
91 | return nil
92 | }
93 |
94 | func (g *gitdb) deleteLockFiles(files []string) error {
95 | var err error
96 | var failedDeletes []string
97 | if len(files) > 0 {
98 | for _, file := range files {
99 | err = os.Remove(file)
100 | if err != nil {
101 | failedDeletes = append(failedDeletes, file)
102 | }
103 | }
104 | }
105 |
106 | if len(failedDeletes) > 0 {
107 | return errors.New("Could not delete lock files: " + strings.Join(failedDeletes, ","))
108 | }
109 |
110 | return err
111 | }
112 |
--------------------------------------------------------------------------------
/log.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | golog "log"
5 |
6 | "github.com/bouggo/log"
7 | )
8 |
9 | //LogLevel is used to set verbosity of GitDB
10 | type LogLevel int
11 |
12 | const (
13 | //LogLevelNone - log nothing
14 | LogLevelNone LogLevel = iota
15 | //LogLevelError - logs only errors
16 | LogLevelError
17 | //LogLevelWarning - logs warning and errors
18 | LogLevelWarning
19 | //LogLevelTest - logs only debug messages
20 | LogLevelTest
21 | //LogLevelInfo - logs info, warining and errors
22 | LogLevelInfo
23 | )
24 |
25 | //SetLogLevel sets log level
26 | func SetLogLevel(l LogLevel) {
27 | log.SetLogLevel(log.Level(l))
28 | }
29 |
30 | //SetLogger sets Logger
31 | func SetLogger(l *golog.Logger) {
32 | log.SetLogger(l)
33 | }
34 |
--------------------------------------------------------------------------------
/mail.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type mail struct {
8 | Subject string
9 | Body string
10 | Date time.Time
11 | }
12 |
13 | func newMail(subject string, body string) *mail {
14 | return &mail{Subject: subject, Body: body, Date: time.Now()}
15 | }
16 |
17 | func (g *gitdb) GetMails() []*mail {
18 | mails := g.mails
19 | g.mails = []*mail{}
20 | return mails
21 | }
22 |
23 | func (g *gitdb) sendMail(m *mail) {
24 | g.mails = append(g.mails, m)
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/mail_test.go:
--------------------------------------------------------------------------------
1 | package gitdb_test
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestMailGetMails(t *testing.T) {
8 | teardown := setup(t, nil)
9 | defer teardown(t)
10 |
11 | mails := testDb.GetMails()
12 | if len(mails) > 0 {
13 | t.Errorf("testDb.GetMails() should be 0")
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/model.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | //Model interface describes methods GitDB supports
8 | type Model interface {
9 | GetSchema() *Schema
10 | //Validate validates a Model
11 | Validate() error
12 | //ShouldEncrypt informs GitDb if a Model support encryption
13 | ShouldEncrypt() bool
14 | //BeforeInsert is called by gitdb before insert
15 | BeforeInsert() error
16 | }
17 |
18 | type LockableModel interface {
19 | //GetLockFileNames informs GitDb of files a Models using for locking
20 | GetLockFileNames() []string
21 | }
22 |
23 | //TimeStampedModel provides time stamp fields
24 | type TimeStampedModel struct {
25 | CreatedAt time.Time
26 | UpdatedAt time.Time
27 | }
28 |
29 | //BeforeInsert implements Model.BeforeInsert
30 | func (m *TimeStampedModel) BeforeInsert() error {
31 | stampTime := time.Now()
32 | if m.CreatedAt.IsZero() {
33 | m.CreatedAt = stampTime
34 | }
35 | m.UpdatedAt = stampTime
36 |
37 | return nil
38 | }
39 |
40 | type model struct {
41 | Version string
42 | Data Model
43 | }
44 |
45 | func wrap(m Model) *model {
46 | return &model{
47 | Version: RecVersion,
48 | Data: m,
49 | }
50 | }
51 |
52 | func (m *model) GetSchema() *Schema {
53 | return m.Data.GetSchema()
54 | }
55 |
56 | func (m *model) Validate() error {
57 | return m.Data.Validate()
58 | }
59 |
60 | func (m *model) ShouldEncrypt() bool {
61 | return m.Data.ShouldEncrypt()
62 | }
63 |
64 | func (m *model) BeforeInsert() error {
65 | err := m.Data.BeforeInsert()
66 | return err
67 | }
68 |
69 | func (g *gitdb) RegisterModel(dataset string, m Model) bool {
70 | if g.registry == nil {
71 | g.registry = make(map[string]Model)
72 | }
73 | g.registry[dataset] = m
74 | return true
75 | }
76 |
77 | func (g *gitdb) isRegistered(dataset string) bool {
78 | if _, ok := g.registry[dataset]; ok {
79 | return true
80 | }
81 |
82 | if g.config.Factory != nil && g.config.Factory(dataset) != nil {
83 | return true
84 | }
85 |
86 | return false
87 | }
88 |
--------------------------------------------------------------------------------
/paths.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "path/filepath"
5 | )
6 |
7 | func (g *gitdb) absDbPath() string {
8 | absDbPath, err := filepath.Abs(g.config.DBPath)
9 | if err != nil {
10 | panic(err)
11 | }
12 |
13 | return absDbPath
14 | }
15 |
16 | func (g *gitdb) dbDir() string {
17 | return filepath.Join(g.absDbPath(), "data")
18 | }
19 |
20 | func (g *gitdb) fullPath(m Model) string {
21 | return filepath.Join(g.dbDir(), m.GetSchema().name())
22 | }
23 |
24 | func (g *gitdb) blockFilePath(dataset, block string) string {
25 | return filepath.Join(g.dbDir(), dataset, block+".json")
26 | }
27 |
28 | func (g *gitdb) lockDir(m Model) string {
29 | return filepath.Join(g.fullPath(m), "Lock")
30 | }
31 |
32 | //index path
33 | func (g *gitdb) indexDir() string {
34 | return filepath.Join(g.absDbPath(), g.internalDirName(), "index")
35 | }
36 |
37 | func (g *gitdb) indexPath(dataset string) string {
38 | return filepath.Join(g.indexDir(), dataset)
39 | }
40 |
41 | //ssh paths
42 | func (g *gitdb) sshDir() string {
43 | return filepath.Join(g.absDbPath(), g.internalDirName(), "ssh")
44 | }
45 |
46 | //ssh paths
47 | func (g *gitdb) publicKeyFilePath() string {
48 | return filepath.Join(g.sshDir(), "gitdb.pub")
49 | }
50 |
51 | func (g *gitdb) privateKeyFilePath() string {
52 | return filepath.Join(g.sshDir(), "gitdb")
53 | }
54 |
55 | func (g *gitdb) internalDirName() string {
56 | return ".gitdb" //todo rename
57 | }
58 |
--------------------------------------------------------------------------------
/read.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "os"
7 | "path/filepath"
8 | "strings"
9 |
10 | "github.com/bouggo/log"
11 | "github.com/gogitdb/gitdb/v2/internal/db"
12 | )
13 |
14 | func (g *gitdb) loadBlock(blockFile string) (*db.Block, error) {
15 |
16 | if g.loadedBlocks == nil {
17 | g.loadedBlocks = map[string]*db.Block{}
18 | }
19 |
20 | //if block file is not cached, load into cache
21 | if _, ok := g.loadedBlocks[blockFile]; !ok {
22 | g.loadedBlocks[blockFile] = db.LoadBlock(blockFile, g.config.EncryptionKey)
23 | }
24 |
25 | return g.loadedBlocks[blockFile], nil
26 | }
27 |
28 | func (g *gitdb) doGet(id string) (*db.Record, error) {
29 |
30 | dataset, block, _, err := ParseID(id)
31 | if err != nil {
32 | return nil, err
33 | }
34 |
35 | if !g.isRegistered(dataset) {
36 | return nil, ErrInvalidDataset
37 | }
38 |
39 | blockFilePath := filepath.Join(g.dbDir(), dataset, block+".json")
40 | if _, err := os.Stat(blockFilePath); err != nil {
41 | return nil, ErrNoRecords
42 | }
43 |
44 | //we used to to a doGetByIndex here but it doesn't work properly
45 | //TODO revisit doGetByIndex
46 | dataBlock := db.NewEmptyBlock(g.config.EncryptionKey)
47 | if err := dataBlock.Hydrate(blockFilePath); err != nil {
48 | return nil, err
49 | }
50 |
51 | return dataBlock.Get(id)
52 | }
53 |
54 | //func (g *gitdb) doGetByIndex(id, dataset string) (*db.Record, error) {
55 | // //read id index
56 | // indexFile := filepath.Join(g.indexDir(), dataset, "id.json")
57 | // if _, ok := g.indexCache[indexFile]; !ok {
58 | // g.buildIndexTargeted(dataset)
59 | // }
60 | //
61 | // iv, ok := g.indexCache[indexFile][id]
62 | // if ok {
63 | // dataBlock := db.NewEmptyBlock(g.config.EncryptionKey)
64 | // err = dataBlock.HydrateByPositions(blockFilePath, []int{iv.Offset, iv.Len})
65 | // if err != nil {
66 | // log.Error(err.Error())
67 | // return nil, fmt.Errorf("Record %s not found in %s", id, dataset)
68 | // }
69 | //
70 | // record, err := dataBlock.Get(id)
71 | // if err != nil {
72 | // log.Error(err.Error())
73 | // return nil, fmt.Errorf("Record %s not found in %s", id, dataset)
74 | // }
75 | //
76 | // return record, nil
77 | // }
78 | //
79 | // return nil, fmt.Errorf("Record %s not found in %s", id, dataset)
80 | //}
81 |
82 | //Get hydrates a model with specified id into result Model
83 | func (g *gitdb) Get(id string, result Model) error {
84 | record, err := g.doGet(id)
85 | if err != nil {
86 | return err
87 | }
88 |
89 | g.events <- newReadEvent("...", id)
90 |
91 | return record.Hydrate(result)
92 | }
93 |
94 | func (g *gitdb) Exists(id string) error {
95 | _, err := g.doGet(id)
96 | if err == nil {
97 | g.events <- newReadEvent("...", id)
98 | }
99 |
100 | return err
101 | }
102 |
103 | func (g *gitdb) Fetch(dataset string, blocks ...string) ([]*db.Record, error) {
104 | if !g.isRegistered(dataset) {
105 | return nil, ErrInvalidDataset
106 | }
107 |
108 | dataBlock := db.NewEmptyBlock(g.config.EncryptionKey)
109 |
110 | if len(blocks) > 0 {
111 | fullPath := filepath.Join(g.dbDir(), dataset)
112 | for _, block := range blocks {
113 | blockFile := filepath.Join(fullPath, block+".json")
114 | log.Test("Fetching BLOCK records from - " + blockFile)
115 | if err := dataBlock.Hydrate(blockFile); err != nil {
116 | return nil, err
117 | }
118 | }
119 |
120 | return dataBlock.Records(), nil
121 | }
122 |
123 | if err := g.doFetch(dataset, dataBlock); err != nil {
124 | return nil, err
125 | }
126 |
127 | log.Info(fmt.Sprintf("%d records found in %s", dataBlock.Len(), dataset))
128 | return dataBlock.Records(), nil
129 | }
130 |
131 | func (g *gitdb) doFetch(dataset string, dataBlock *db.EmptyBlock) error {
132 |
133 | fullPath := filepath.Join(g.dbDir(), dataset)
134 | //events <- newReadEvent("...", fullPath)
135 | log.Info("Fetching records from - " + fullPath)
136 | files, err := ioutil.ReadDir(fullPath)
137 | if err != nil && !os.IsNotExist(err) {
138 | return err
139 | }
140 |
141 | if os.IsNotExist(err) {
142 | return ErrNoRecords
143 | }
144 |
145 | var fileName string
146 | for _, file := range files {
147 | fileName = filepath.Join(fullPath, file.Name())
148 | if filepath.Ext(fileName) == ".json" {
149 | if err := dataBlock.Hydrate(fileName); err != nil {
150 | return err
151 | }
152 | }
153 | }
154 |
155 | return nil
156 | }
157 |
158 | func (g *gitdb) Search(dataset string, searchParams []*SearchParam, searchMode SearchMode) ([]*db.Record, error) {
159 | if !g.isRegistered(dataset) {
160 | return nil, ErrInvalidDataset
161 | }
162 |
163 | //searchBlocks return the position of the record in the block
164 | //searchBlocks := map[string][][]int{} //index based
165 | searchBlocks := map[string]bool{}
166 | matchingRecords := map[string]string{}
167 |
168 | for _, searchParam := range searchParams {
169 | indexFile := filepath.Join(g.indexDir(), dataset, searchParam.Index+".json")
170 | if _, ok := g.indexCache[indexFile]; !ok {
171 | g.buildIndexTargeted(dataset)
172 | }
173 |
174 | g.events <- newReadEvent("...", indexFile)
175 |
176 | queryValue := strings.ToLower(searchParam.Value)
177 | for recordID, iv := range g.indexCache[indexFile] {
178 | addResult := false
179 | dbValue := strings.ToLower(iv.(string))
180 | switch searchMode {
181 | case SearchEquals:
182 | addResult = dbValue == queryValue
183 | case SearchContains:
184 | addResult = strings.Contains(dbValue, queryValue)
185 | case SearchStartsWith:
186 | addResult = strings.HasPrefix(dbValue, queryValue)
187 | case SearchEndsWith:
188 | addResult = strings.HasSuffix(dbValue, queryValue)
189 | }
190 |
191 | if addResult {
192 | dataset, block, _, err := ParseID(recordID)
193 | if err != nil {
194 | return nil, err
195 | }
196 |
197 | matchingRecords[recordID] = recordID
198 | searchBlocks[g.blockFilePath(dataset, block)] = true
199 | }
200 | }
201 | }
202 |
203 | resultBlock := db.NewEmptyBlock(g.config.EncryptionKey)
204 | //TODO revisit index based search
205 | //for block, pos := range searchBlocks {
206 | // blockFile := filepath.Join(g.dbDir(), dataset, block+".json")
207 | // err := resultBlock.HydrateByPositions(blockFile, pos...)
208 | // if err != nil {
209 | // return nil, err
210 | // }
211 | //}
212 |
213 | for block := range searchBlocks {
214 | if err := resultBlock.Hydrate(block); err != nil {
215 | log.Error(err.Error())
216 | continue
217 | }
218 | }
219 |
220 | resultBlock.Filter(matchingRecords)
221 | return resultBlock.Records(), nil
222 | }
223 |
--------------------------------------------------------------------------------
/read_test.go:
--------------------------------------------------------------------------------
1 | package gitdb_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/bouggo/log"
7 | "github.com/gogitdb/gitdb/v2"
8 | )
9 |
10 | //TODO write negative tests (e.g record not found)
11 | func TestGet(t *testing.T) {
12 | teardown := setup(t, getReadTestConfig(gitdb.RecVersion))
13 | defer teardown(t)
14 |
15 | m := getTestMessage()
16 |
17 | recId := gitdb.ID(m)
18 | result := &Message{}
19 | err := testDb.Get(recId, result)
20 | if err != nil {
21 | t.Error(err.Error())
22 | }
23 |
24 | if err == nil && gitdb.ID(result) != recId {
25 | t.Errorf("Want: %v, Got: %v", recId, gitdb.ID(result))
26 | }
27 | }
28 |
29 | func TestGetV1(t *testing.T) {
30 | teardown := setup(t, getReadTestConfig("v1"))
31 | defer teardown(t)
32 |
33 | m := getTestMessage()
34 |
35 | recId := gitdb.ID(m)
36 | result := &Message{}
37 | err := testDb.Get(recId, result)
38 | if err != nil {
39 | t.Error(err.Error())
40 | }
41 |
42 | if err == nil && gitdb.ID(result) != recId {
43 | t.Errorf("Want: %v, Got: %v", recId, gitdb.ID(result))
44 | }
45 | }
46 |
47 | func TestExists(t *testing.T) {
48 | teardown := setup(t, getReadTestConfig(gitdb.RecVersion))
49 | defer teardown(t)
50 |
51 | m := getTestMessage()
52 |
53 | recId := gitdb.ID(m)
54 | err := testDb.Exists(recId)
55 | if err != nil {
56 | t.Error(err.Error())
57 | }
58 | }
59 |
60 | func TestFetch(t *testing.T) {
61 | teardown := setup(t, getReadTestConfig(gitdb.RecVersion))
62 | defer teardown(t)
63 |
64 | dataset := "Message"
65 | messages, err := testDb.Fetch(dataset)
66 | if err != nil {
67 | t.Error(err.Error())
68 | }
69 |
70 | want := 10
71 | got := len(messages)
72 | if got != want {
73 | t.Errorf("Want: %d, Got: %d", want, got)
74 | }
75 | }
76 |
77 | func TestFetchBlock(t *testing.T) {
78 | teardown := setup(t, getReadTestConfig(gitdb.RecVersion))
79 | defer teardown(t)
80 |
81 | dataset := "Message"
82 | messages, err := testDb.Fetch(dataset, "b0")
83 | if err != nil {
84 | t.Error(err.Error())
85 | }
86 |
87 | want := 10
88 | got := len(messages)
89 | if got != want {
90 | t.Errorf("Want: %d, Got: %d", want, got)
91 | }
92 | }
93 |
94 | //TODO test correctness of search results
95 | func TestSearch(t *testing.T) {
96 | teardown := setup(t, getReadTestConfig(gitdb.RecVersion))
97 | defer teardown(t)
98 |
99 | count := 10
100 | sp := &gitdb.SearchParam{
101 | Index: "From",
102 | Value: "alice@example.com",
103 | }
104 |
105 | results, err := testDb.Search("Message", []*gitdb.SearchParam{sp}, gitdb.SearchEquals)
106 | if err != nil {
107 | t.Errorf("search failed with error - %s", err)
108 | return
109 | }
110 |
111 | if len(results) != count {
112 | t.Errorf("search result count wrong. want: %d, got: %d", count, len(results))
113 | }
114 | }
115 |
116 | func BenchmarkFetch(b *testing.B) {
117 | teardown := setup(b, getReadTestConfig(gitdb.RecVersion))
118 | defer teardown(b)
119 |
120 | b.ReportAllocs()
121 | for i := 0; i <= b.N; i++ {
122 | testDb.Fetch("Message")
123 | }
124 | }
125 |
126 | func BenchmarkGet(b *testing.B) {
127 | teardown := setup(b, getReadTestConfig(gitdb.RecVersion))
128 | defer teardown(b)
129 |
130 | b.ReportAllocs()
131 | m := &Message{}
132 | for i := 0; i <= b.N; i++ {
133 | testDb.Get("Message/b0/1", m)
134 | log.Test(m.Body)
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/schema.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "io/ioutil"
8 | "os"
9 | "path/filepath"
10 | "strings"
11 |
12 | "github.com/bouggo/log"
13 | )
14 |
15 | //Schema holds functions for generating a model id
16 | type Schema struct {
17 | dataset string
18 | block string
19 | record string
20 | indexes map[string]interface{}
21 |
22 | internal bool
23 | }
24 |
25 | //NewSchema constructs a *Schema
26 | func NewSchema(name, block, record string, indexes map[string]interface{}) *Schema {
27 | return &Schema{dataset: name, block: block, record: record, indexes: indexes}
28 | }
29 |
30 | func newSchema(name, block, record string, indexes map[string]interface{}) *Schema {
31 | return &Schema{dataset: name, block: block, record: record, indexes: indexes, internal: true}
32 | }
33 |
34 | //name returns name of schema
35 | func (a *Schema) name() string {
36 | return a.dataset
37 | }
38 |
39 | //blockID retuns block id of schema
40 | func (a *Schema) blockID() string {
41 | return a.dataset + "/" + a.block
42 | }
43 |
44 | //recordID returns record id of schema
45 | func (a *Schema) recordID() string {
46 | return a.blockID() + "/" + a.record
47 | }
48 |
49 | //Validate ensures *Schema is valid
50 | func (a *Schema) Validate() error {
51 | if len(a.dataset) == 0 {
52 | return errors.New("Invalid Schema Name")
53 | }
54 |
55 | if !a.internal && !a.validDatasetName(a.dataset) {
56 | return fmt.Errorf("%s is a reserved Schema Name", a.dataset)
57 | }
58 |
59 | if !a.validName(a.block) {
60 | return errors.New("Invalid Schema Block ID")
61 | }
62 |
63 | if !a.validName(a.record) {
64 | return errors.New("Invalid Schema Record ID")
65 | }
66 |
67 | if _, ok := a.indexes["id"]; ok && !a.internal {
68 | return fmt.Errorf("%s is a reserved index name", "id")
69 | }
70 |
71 | return nil
72 | }
73 |
74 | func (a *Schema) validDatasetName(name string) bool {
75 | reservedName := []string{"gitdb", "bucket", "upload"}
76 | lcname := strings.ToLower(name)
77 | for _, rname := range reservedName {
78 | if lcname == rname {
79 | return false
80 | }
81 | }
82 |
83 | return a.validName(name)
84 | }
85 |
86 | func (a *Schema) validName(name string) bool {
87 | if len(name) < 1 {
88 | return false
89 | }
90 |
91 | allowedChars := `abcdefghijklmnopqrstuvwxyz0123456789_-.`
92 | return strings.ContainsAny(strings.ToLower(name), allowedChars)
93 | }
94 |
95 | //Indexes returns the index map of a given Model
96 | func Indexes(m Model) map[string]interface{} {
97 | return m.GetSchema().indexes
98 | }
99 |
100 | //ID returns the id of a given Model
101 | func ID(m Model) string {
102 | return m.GetSchema().recordID()
103 | }
104 |
105 | //ParseID parses a record id and returns it's metadata
106 | func ParseID(id string) (dataset string, block string, record string, err error) {
107 | recordMeta := strings.Split(id, "/")
108 | if len(recordMeta) != 3 {
109 | err = ErrInvalidRecordID
110 | } else {
111 | dataset = recordMeta[0]
112 | block = recordMeta[1]
113 | record = recordMeta[2]
114 | }
115 |
116 | return dataset, block, record, err
117 | }
118 |
119 | //BlockMethod type of method to use with AutoBlock
120 | type BlockMethod string
121 |
122 | var (
123 | //BlockBySize generates a new block when current block has reached a specified size
124 | BlockBySize BlockMethod = "size"
125 | //BlockByCount generates a new block when the number of records has reached a specified count
126 | BlockByCount BlockMethod = "count"
127 | )
128 |
129 | //AutoBlock automatically generates block id for a given Model depending on a BlockMethod
130 | func AutoBlock(dbPath string, m Model, method BlockMethod, n int64) string {
131 |
132 | var currentBlock int
133 | var currentBlockFile os.FileInfo
134 | var currentBlockrecords map[string]interface{}
135 |
136 | //being sensible
137 | if n <= 0 {
138 | n = 1000
139 | }
140 |
141 | dataset := m.GetSchema().name()
142 | fullPath := filepath.Join(dbPath, "data", dataset)
143 |
144 | if _, err := os.Stat(fullPath); err != nil {
145 | return fmt.Sprintf("b%d", currentBlock)
146 | }
147 |
148 | files, err := ioutil.ReadDir(fullPath)
149 | if err != nil {
150 | log.Error(err.Error())
151 | log.Test("AutoBlock: " + err.Error())
152 | return ""
153 | }
154 |
155 | if len(files) == 0 {
156 | log.Test("AutoBlock: no blocks found at " + fullPath)
157 | return fmt.Sprintf("b%d", currentBlock)
158 | }
159 |
160 | currentBlock = -1
161 | for _, currentBlockFile = range files {
162 | currentBlockFileName := filepath.Join(fullPath, currentBlockFile.Name())
163 | if filepath.Ext(currentBlockFileName) != ".json" {
164 | continue
165 | }
166 |
167 | currentBlock++
168 | //TODO OPTIMIZE read file
169 | b, err := ioutil.ReadFile(currentBlockFileName)
170 | if err != nil {
171 | log.Test("AutoBlock: " + err.Error())
172 | log.Error(err.Error())
173 | continue
174 | }
175 |
176 | currentBlockrecords = make(map[string]interface{})
177 | if err := json.Unmarshal(b, ¤tBlockrecords); err != nil {
178 | log.Error(err.Error())
179 | continue
180 | }
181 |
182 | block := strings.Replace(filepath.Base(currentBlockFileName), filepath.Ext(currentBlockFileName), "", 1)
183 | id := fmt.Sprintf("%s/%s/%s", dataset, block, m.GetSchema().record)
184 |
185 | log.Test("AutoBlock: searching for - " + id)
186 | //model already exists return its block
187 | if _, ok := currentBlockrecords[id]; ok {
188 | log.Test("AutoBlock: found - " + id)
189 | return block
190 | }
191 | }
192 |
193 | //is current block at it's size limit?
194 | if method == BlockBySize && currentBlockFile.Size() >= n {
195 | currentBlock++
196 | return fmt.Sprintf("b%d", currentBlock)
197 | }
198 |
199 | //record size check
200 | log.Test(fmt.Sprintf("AutoBlock: current block count - %d", len(currentBlockrecords)))
201 | if method == BlockByCount && len(currentBlockrecords) >= int(n) {
202 | currentBlock++
203 | }
204 |
205 | return fmt.Sprintf("b%d", currentBlock)
206 | }
207 |
--------------------------------------------------------------------------------
/schema_test.go:
--------------------------------------------------------------------------------
1 | package gitdb_test
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/gogitdb/gitdb/v2"
8 | )
9 |
10 | func TestAutoBlock(t *testing.T) {
11 | cfg := getReadTestConfig(gitdb.RecVersion)
12 | teardown := setup(t, cfg)
13 | defer teardown(t)
14 |
15 | m := getTestMessage()
16 |
17 | want := "b0"
18 | got := gitdb.AutoBlock(cfg.DBPath, m, gitdb.BlockByCount, 10)
19 | if got != want {
20 | t.Errorf("want: %s, got: %s", want, got)
21 | }
22 |
23 | m.MessageId = 11
24 | want = "b1"
25 | got = gitdb.AutoBlock(cfg.DBPath, m, gitdb.BlockByCount, 10)
26 | if got != want {
27 | t.Errorf("want: %s, got: %s", want, got)
28 | }
29 |
30 | m.MessageId = 11
31 | want = "b0"
32 | got = gitdb.AutoBlock("/non/existent/path", m, gitdb.BlockByCount, 10)
33 | if got != want {
34 | t.Errorf("want: %s, got: %s", want, got)
35 | }
36 | }
37 |
38 | func TestHydrate(t *testing.T) {
39 | teardown := setup(t, getReadTestConfig(gitdb.RecVersion))
40 | defer teardown(t)
41 |
42 | result := &Message{}
43 | records, err := testDb.Fetch("Message")
44 | if err != nil {
45 | t.Errorf("testDb.Fetch failed: %s", err)
46 | }
47 |
48 | err = records[0].Hydrate(result)
49 | if err != nil {
50 | t.Errorf("record.Hydrate failed: %s", err)
51 | }
52 | }
53 |
54 | func TestParseId(t *testing.T) {
55 | testId := "DatasetName/Block/RecordId"
56 | ds, block, recordId, err := gitdb.ParseID(testId)
57 |
58 | passed := ds == "DatasetName" && block == "Block" && recordId == "RecordId" && err == nil
59 | if !passed {
60 | t.Errorf("want: DatasetName|Block|RecordId, Got:%s|%s|%s", ds, block, recordId)
61 | }
62 | }
63 |
64 | func TestValidate(t *testing.T) {
65 | cases := []struct {
66 | dataset string
67 | block string
68 | record string
69 | indexes map[string]interface{}
70 | pass bool
71 | }{
72 | {"d1", "b0", "r0", nil, true},
73 | {"", "b0", "r0", nil, false},
74 | {"d1", "", "r0", nil, false},
75 | {"d1", "b0", "", nil, false},
76 | {"", "", "", nil, false},
77 | }
78 |
79 | for _, tc := range cases {
80 | t.Run(fmt.Sprintf("%s/%s/%s", tc.dataset, tc.block, tc.record), func(t *testing.T) {
81 | err := gitdb.NewSchema(tc.dataset, tc.block, tc.record, tc.indexes).Validate()
82 | if (err == nil) != tc.pass {
83 | t.Errorf("test failed: %s", err)
84 | }
85 | })
86 | }
87 | }
88 |
89 | func BenchmarkParseId(b *testing.B) {
90 | b.ReportAllocs()
91 | for i := 0; i <= b.N; i++ {
92 | gitdb.ParseID("DatasetName/Block/RecordId")
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/ssh.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "crypto/rand"
5 | "crypto/rsa"
6 | "crypto/x509"
7 | "encoding/pem"
8 | "io/ioutil"
9 | "os"
10 |
11 | "github.com/bouggo/log"
12 | "golang.org/x/crypto/ssh"
13 | )
14 |
15 | // generateSSHKeyPair make a pair of public and private keys for SSH access.
16 | // Public key is encoded in the format for inclusion in an OpenSSH authorized_keys file.
17 | // Private Key generated is PEM encoded
18 | func (g *gitdb) generateSSHKeyPair() error {
19 |
20 | if _, err := os.Stat(g.privateKeyFilePath()); err == nil {
21 |
22 | if _, err := os.Stat(g.publicKeyFilePath()); err == nil {
23 | return nil
24 | }
25 |
26 | log.Info("Re-generating public key")
27 | //public key is missing - recreate public key
28 | pkPem, err := ioutil.ReadFile(g.privateKeyFilePath())
29 | if err != nil {
30 | return err
31 | }
32 |
33 | b, _ := pem.Decode(pkPem)
34 | privateKey, err := x509.ParsePKCS1PrivateKey(b.Bytes)
35 | if err != nil {
36 | return err
37 | }
38 |
39 | return g.generatePublicKey(privateKey)
40 | }
41 |
42 | if err := os.MkdirAll(g.sshDir(), os.ModePerm); err != nil {
43 | return err
44 | }
45 |
46 | log.Info("Generating ssh key pairs")
47 | privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
48 | if err != nil {
49 | return err
50 | }
51 |
52 | err = g.generatePrivateKey(privateKey)
53 | if err != nil {
54 | return err
55 | }
56 |
57 | return g.generatePublicKey(privateKey)
58 | }
59 |
60 | func (g *gitdb) generatePrivateKey(pk *rsa.PrivateKey) error {
61 | // generate and write private key as PEM
62 | privateKeyFile, err := os.OpenFile(g.privateKeyFilePath(), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400)
63 | defer privateKeyFile.Close()
64 | if err != nil {
65 | return err
66 | }
67 | privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(pk)}
68 | if err := pem.Encode(privateKeyFile, privateKeyPEM); err != nil {
69 | return err
70 | }
71 |
72 | return nil
73 | }
74 |
75 | func (g *gitdb) generatePublicKey(pk *rsa.PrivateKey) error {
76 | // generate and write public key
77 | pub, err := ssh.NewPublicKey(&pk.PublicKey)
78 | if err != nil {
79 | return err
80 | }
81 | return ioutil.WriteFile(g.publicKeyFilePath(), ssh.MarshalAuthorizedKey(pub), 0655)
82 | }
83 |
--------------------------------------------------------------------------------
/static/css/app.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 0;
3 | margin: 0;
4 | font-family: Arial, Helvetica, sans-serif;
5 | }
6 |
7 | div {
8 | box-sizing: border-box;
9 | }
10 |
11 | h1 {
12 | padding: 0;
13 | margin: 0;
14 | margin-bottom: 30px;
15 | }
16 |
17 | h1 a {
18 | text-decoration: none;
19 | color: darkseagreen;
20 | }
21 |
22 | .sidebar {
23 | float: left;
24 | width: 20%;
25 | height: 800px;
26 | background-color: #eee;
27 | border-right: 1px solid #ddd;
28 | padding: 10px;
29 | }
30 |
31 | .content {
32 | padding: 30px;
33 | padding-top: 10px;
34 | float: left;
35 | width: 80%;
36 | height: 800px;
37 | }
38 |
39 | .nav {
40 | list-style: none;
41 | margin: 0;
42 | padding: 0
43 | }
44 |
45 | .nav li {
46 | color: #000;
47 | }
48 |
49 | .nav a {
50 | color: #000;
51 | text-decoration: none;
52 | display: block;
53 | padding-top: 10px;
54 | padding-bottom: 5px;
55 | padding-left: 5px;
56 | border-bottom: 1px solid #ddd;
57 | }
58 |
59 | .nav a:hover {
60 | background-color: #ddd;
61 | }
62 |
63 | table tr:hover td {
64 | cursor: pointer;
65 | background-color: #ccc;
66 | }
67 |
68 | table th {
69 | background-color: darkseagreen;
70 | color: #fff;
71 | text-align: left;
72 | }
73 |
74 | table {
75 | width: 100%;
76 | /* border: 1px solid #000; */
77 | border-spacing: 0px;
78 | }
79 |
80 | table td,
81 | table th {
82 | padding: 10px;
83 | border-bottom: 1px solid #ddd;
84 | }
85 |
86 | pre {
87 | background-color: #222;
88 | color: #fff;
89 | padding: 10px;
90 | font-size: 14px;
91 | width: 800px;
92 | overflow: hidden;
93 | }
94 |
95 | textarea {
96 | display: block;
97 | }
98 |
99 | .listWindow {
100 | width: 100%;
101 | overflow-x: scroll;
102 | }
--------------------------------------------------------------------------------
/static/errors.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{template "sidebar" $}}
8 |
9 |
{{.Title}}
10 |
11 | {{if .DataSet.BadBlocks}}
12 |
Bad Blocks
13 |
14 | {{range $key, $value := .DataSet.BadBlocks}}
15 | {{ $value }}
16 | {{end}}
17 |
18 | {{end}} {{if .DataSet.BadRecords}}
19 |
Bad Records
20 |
21 | {{range $key, $value := .DataSet.BadRecords}}
22 | {{ $value }}
23 | {{end}}
24 |
25 | {{end}}
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{template "sidebar" $}}
9 |
10 |
{{.Title}}
11 |
12 |
13 |
14 | Dataset
15 | No. of blocks
16 | No. of records
17 | Size
18 | Errors
19 | Indexes
20 | Last Modified
21 |
22 | {{range $key, $value := .DataSets}}
23 |
24 | {{ $value.Name }}
25 | {{ $value.BlockCount }}
26 | {{ $value.RecordCount }}
27 | {{ $value.HumanSize }}
28 | {{ $value.BadBlocksCount }} block(s) / {{ $value.BadRecordsCount }} record(s)
29 |
30 |
31 | {{range $indexName := $value.Indexes}}
32 | {{ $indexName }}
33 | {{end}}
34 |
35 |
36 | {{ $value.LastModifiedDate }}
37 |
38 | {{end}}
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/static/js/app.js:
--------------------------------------------------------------------------------
1 | window.addEventListener('load', (event) => {
2 | makeDatasetRowsClickable();
3 | makeRecordRowsClickable();
4 | });
5 |
6 | function makeDatasetRowsClickable() {
7 | document.querySelectorAll('.datasetRow').forEach(row => {
8 | row.addEventListener('click', event => {
9 | window.location = row.dataset.view
10 | });
11 | })
12 | }
13 |
14 | function makeRecordRowsClickable() {
15 | document.querySelectorAll('.recordRow').forEach(row => {
16 | row.addEventListener('click', event => {
17 | window.location = row.dataset.view
18 | });
19 | })
20 | }
--------------------------------------------------------------------------------
/static/list.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{template "sidebar" $}}
9 |
10 |
{{.DataSet.Name}}
11 |
{{.DataSet.BlockCount}} blocks {{.DataSet.HumanSize}}
12 |
13 |
14 |
15 |
16 | {{range $key, $value := .Table.Headers}}
17 | {{ $value }}
18 | {{end}}
19 |
20 | {{range $key, $value := .Table.Rows}}
21 |
22 | {{range $k, $v := $value}} {{if eq $k 0}}
23 | {{ $v }}
24 | {{else}}
25 | {{ $v }}
26 | {{end}} {{end}}
27 |
28 | {{end}}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/static/sidebar.html:
--------------------------------------------------------------------------------
1 | {{define "sidebar"}}
2 |
11 | {{end}}
--------------------------------------------------------------------------------
/static/view.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{template "sidebar" $}}
9 |
10 |
{{.DataSet.Name}}
11 |
{{.DataSet.BlockCount}} blocks {{.Block.HumanSize}}/{{.DataSet.HumanSize}}
12 |
13 |
Prev Block |
Next Block
14 |
15 | {{.Content}}
16 |
17 |
Prev Record |
Next Record
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/sync.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "fmt"
5 | "github.com/bouggo/log"
6 | "time"
7 | )
8 |
9 | func (g *gitdb) Sync() error {
10 | g.syncMu.Lock()
11 | defer g.syncMu.Unlock()
12 |
13 | if len(g.config.OnlineRemote) == 0 {
14 | return ErrNoOnlineRemote
15 | }
16 |
17 | // if client PC has at least 20% battery life
18 | if !hasSufficientBatteryPower(20) {
19 | return ErrLowBattery
20 | }
21 |
22 | log.Info("Syncing database...")
23 | changedFiles := g.driver.changedFiles()
24 | if err := g.driver.sync(); err != nil {
25 | log.Error(err.Error())
26 | return ErrDBSyncFailed
27 | }
28 |
29 | // reset loaded blocks
30 | g.loadedBlocks = nil
31 |
32 | g.buildIndexSmart(changedFiles)
33 | return nil
34 | }
35 |
36 | func (g *gitdb) startSyncClock() {
37 | go func(g *gitdb) {
38 | log.Test(fmt.Sprintf("starting sync clock @ interval %s", g.config.SyncInterval))
39 | ticker := time.NewTicker(g.config.SyncInterval)
40 | for {
41 | select {
42 | case <-g.shutdown:
43 | log.Test("shutting down sync clock")
44 | return
45 | case <-ticker.C:
46 | g.writeMu.Lock()
47 | if err := g.Sync(); err != nil {
48 | log.Error(err.Error())
49 | }
50 | g.writeMu.Unlock()
51 | }
52 | }
53 | }(g)
54 | }
55 |
--------------------------------------------------------------------------------
/sync_test.go:
--------------------------------------------------------------------------------
1 | package gitdb_test
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | "testing"
7 | )
8 |
9 | func TestSync(t *testing.T) {
10 | t.Skip()
11 | cfg := getConfig()
12 | cfg.DBPath = "/tmp/voguedb"
13 | cfg.OnlineRemote = "git@bitbucket.org:voguehotel/db-dev.git"
14 | cfg.EncryptionKey = ""
15 | cfg.SyncInterval = 0 //disables sync clock
16 | teardown := setup(t, cfg)
17 | defer teardown(t)
18 |
19 | wg := sync.WaitGroup{}
20 | n := 10
21 | for {
22 | if n <= 0 {
23 | break
24 | }
25 | wg.Add(1)
26 | fmt.Println("dispatching ", n)
27 | go func() {
28 | if err := testDb.Sync(); err != nil {
29 | t.Errorf("db.Sync failed: %s", err)
30 | }
31 | wg.Done()
32 | }()
33 | n--
34 | }
35 |
36 | wg.Wait()
37 | }
38 |
--------------------------------------------------------------------------------
/testdata/v1/data/data/Message/b0.json:
--------------------------------------------------------------------------------
1 | {
2 | "Message/b0/0": "{\"CreatedAt\":\"2020-03-29T19:02:36.576218+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.576218+01:00\",\"MessageId\":0,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}",
3 | "Message/b0/1": "{\"CreatedAt\":\"2020-03-29T19:02:36.577797+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.577797+01:00\",\"MessageId\":1,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}",
4 | "Message/b0/2": "{\"CreatedAt\":\"2020-03-29T19:02:36.578336+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.578336+01:00\",\"MessageId\":2,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}",
5 | "Message/b0/3": "{\"CreatedAt\":\"2020-03-29T19:02:36.578835+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.578835+01:00\",\"MessageId\":3,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}",
6 | "Message/b0/4": "{\"CreatedAt\":\"2020-03-29T19:02:36.579284+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.579284+01:00\",\"MessageId\":4,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}",
7 | "Message/b0/5": "{\"CreatedAt\":\"2020-03-29T19:02:36.57987+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.57987+01:00\",\"MessageId\":5,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}",
8 | "Message/b0/6": "{\"CreatedAt\":\"2020-03-29T19:02:36.580396+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.580396+01:00\",\"MessageId\":6,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}",
9 | "Message/b0/7": "{\"CreatedAt\":\"2020-03-29T19:02:36.581039+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.581039+01:00\",\"MessageId\":7,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}",
10 | "Message/b0/8": "{\"CreatedAt\":\"2020-03-29T19:02:36.58172+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.58172+01:00\",\"MessageId\":8,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}",
11 | "Message/b0/9": "{\"CreatedAt\":\"2020-03-29T19:02:36.582319+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.582319+01:00\",\"MessageId\":9,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}"
12 | }
13 |
--------------------------------------------------------------------------------
/testdata/v1/data/data/MessageV2/202003.json:
--------------------------------------------------------------------------------
1 | {
2 | "MessageV2/202003/0": "{\"CreatedAt\":\"2020-03-29T19:00:32.994814+01:00\",\"UpdatedAt\":\"2020-03-29T19:00:33.06084+01:00\",\"MessageId\":0,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}"
3 | }
4 |
--------------------------------------------------------------------------------
/testdata/v2/data/data/Message/b0.json:
--------------------------------------------------------------------------------
1 | {
2 | "Message/b0/0": "{\"Version\":\"v2\",\"Indexes\":{\"From\":\"alice@example.com\"},\"Data\":{\"CreatedAt\":\"2020-03-29T19:02:36.576218+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.576218+01:00\",\"MessageId\":0,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}}",
3 | "Message/b0/1": "{\"Version\":\"v2\",\"Indexes\":{\"From\":\"alice@example.com\"},\"Data\":{\"CreatedAt\":\"2020-03-29T19:02:36.577797+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.577797+01:00\",\"MessageId\":1,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}}",
4 | "Message/b0/2": "{\"Version\":\"v2\",\"Indexes\":{\"From\":\"alice@example.com\"},\"Data\":{\"CreatedAt\":\"2020-03-29T19:02:36.578336+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.578336+01:00\",\"MessageId\":2,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}}",
5 | "Message/b0/3": "{\"Version\":\"v2\",\"Indexes\":{\"From\":\"alice@example.com\"},\"Data\":{\"CreatedAt\":\"2020-03-29T19:02:36.578835+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.578835+01:00\",\"MessageId\":3,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}}",
6 | "Message/b0/4": "{\"Version\":\"v2\",\"Indexes\":{\"From\":\"alice@example.com\"},\"Data\":{\"CreatedAt\":\"2020-03-29T19:02:36.579284+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.579284+01:00\",\"MessageId\":4,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}}",
7 | "Message/b0/5": "{\"Version\":\"v2\",\"Indexes\":{\"From\":\"alice@example.com\"},\"Data\":{\"CreatedAt\":\"2020-03-29T19:02:36.57987+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.57987+01:00\",\"MessageId\":5,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}}",
8 | "Message/b0/6": "{\"Version\":\"v2\",\"Indexes\":{\"From\":\"alice@example.com\"},\"Data\":{\"CreatedAt\":\"2020-03-29T19:02:36.580396+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.580396+01:00\",\"MessageId\":6,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}}",
9 | "Message/b0/7": "{\"Version\":\"v2\",\"Indexes\":{\"From\":\"alice@example.com\"},\"Data\":{\"CreatedAt\":\"2020-03-29T19:02:36.581039+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.581039+01:00\",\"MessageId\":7,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}}",
10 | "Message/b0/8": "{\"Version\":\"v2\",\"Indexes\":{\"From\":\"alice@example.com\"},\"Data\":{\"CreatedAt\":\"2020-03-29T19:02:36.58172+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.58172+01:00\",\"MessageId\":8,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}}",
11 | "Message/b0/9": "{\"Version\":\"v2\",\"Indexes\":{\"From\":\"alice@example.com\"},\"Data\":{\"CreatedAt\":\"2020-03-29T19:02:36.582319+01:00\",\"UpdatedAt\":\"2020-03-29T19:02:36.582319+01:00\",\"MessageId\":9,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}}"
12 | }
13 |
--------------------------------------------------------------------------------
/testdata/v2/data/data/MessageV2/202003.json:
--------------------------------------------------------------------------------
1 | {
2 | "MessageV2/202003/0": "{\"Version\":\"v2\",\"Indexes\":{\"From\":\"alice@example.com\"},\"Data\":{\"CreatedAt\":\"2020-03-29T19:00:32.994814+01:00\",\"UpdatedAt\":\"2020-03-29T19:00:33.06084+01:00\",\"MessageId\":0,\"From\":\"alice@example.com\",\"To\":\"bob@example.com\",\"Body\":\"Hello\"}}"
3 | }
4 |
--------------------------------------------------------------------------------
/tools/fix_corrupt_head.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
--------------------------------------------------------------------------------
/tools/fix_corrupt_missing_objects.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | #https://stackoverflow.com/questions/33012869/broken-branch-in-git-fatal-your-current-branch-appears-to-be-broken
4 |
5 | git fsck --full >& fsck_report.txt
6 | cat fsck_report.txt | grep "missing" | awk '{print $7}' > fsck_report.txt
7 |
8 |
9 | Did it report a corrupted file?
10 | If so delete the file, go back to step #1.
11 |
12 | for item in `cat fsck_report.txt`
13 | do
14 | echo "deleting $item"
15 | rm -f $item
16 | done
17 |
18 | Do del .git/index
19 | Do git reset
20 |
21 | rm -f fsck_report
22 |
23 |
24 | #remove invalid reflog references
25 | git reflog expire --stale-fix --all
26 |
27 | clone down repo to a separate dir
28 | and run
29 | cat ../fresh/.git/objects/pack/pack-*.pack | git unpack-objects
30 | within the broken repo
--------------------------------------------------------------------------------
/transaction.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/bouggo/log"
7 | )
8 |
9 | type operation func() error
10 |
11 | // Transaction represents a db transaction
12 | type Transaction interface {
13 | Commit() error
14 | AddOperation(o operation)
15 | }
16 |
17 | type transaction struct {
18 | name string
19 | operations []operation
20 | db *gitdb
21 | }
22 |
23 | func (t *transaction) Commit() error {
24 | t.db.autoCommit = false
25 | for _, o := range t.operations {
26 | if err := o(); err != nil {
27 | log.Info("Reverting transaction: " + err.Error())
28 | err2 := t.db.driver.undo()
29 | t.db.autoCommit = true
30 | if err2 != nil {
31 | err = fmt.Errorf("%s - %s", err.Error(), err2.Error())
32 | }
33 |
34 | return err
35 | }
36 | }
37 |
38 | t.db.autoCommit = true
39 | commitMsg := "Committing transaction: " + t.name
40 | t.db.commit.Add(1)
41 | t.db.events <- newWriteEvent(commitMsg, ".", t.db.autoCommit)
42 | t.db.waitForCommit()
43 | return nil
44 | }
45 |
46 | func (t *transaction) AddOperation(o operation) {
47 | t.operations = append(t.operations, o)
48 | }
49 |
50 | func (g *gitdb) StartTransaction(name string) Transaction {
51 | return &transaction{name: name, db: g}
52 | }
53 |
--------------------------------------------------------------------------------
/transaction_test.go:
--------------------------------------------------------------------------------
1 | package gitdb_test
2 |
3 | import (
4 | "errors"
5 | "testing"
6 |
7 | "github.com/gogitdb/gitdb/v2"
8 | )
9 |
10 | func TestTransaction(t *testing.T) {
11 | //intentionally crafted to increase converage on *gitdb.configure
12 | cfg := &gitdb.Config{
13 | DBPath: dbPath,
14 | }
15 | teardown := setup(t, cfg)
16 | defer teardown(t)
17 |
18 | tx := testDb.StartTransaction("test")
19 | tx.AddOperation(func() error { return nil })
20 | tx.AddOperation(func() error { return nil })
21 | tx.AddOperation(func() error { return errors.New("test error") })
22 | tx.AddOperation(func() error { return nil })
23 | if err := tx.Commit(); err == nil {
24 | t.Error("transaction should fail on 3rd operation")
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/ui.go:
--------------------------------------------------------------------------------
1 | //go:generate gitdb embed-ui -o ./ui_static.go
2 | package gitdb
3 |
4 | import (
5 | "context"
6 | "encoding/base64"
7 | "fmt"
8 | "html/template"
9 | "io/ioutil"
10 | "net/http"
11 | "path/filepath"
12 | "time"
13 |
14 | "github.com/bouggo/log"
15 | "github.com/gogitdb/gitdb/v2/internal/db"
16 | "github.com/gorilla/mux"
17 | )
18 |
19 | var fs *fileSystem
20 |
21 | func getFs() *fileSystem {
22 | if fs == nil {
23 | fs = &fileSystem{}
24 | }
25 | return fs
26 | }
27 |
28 | func (g *gitdb) startUI() {
29 |
30 | server := &http.Server{
31 | Addr: fmt.Sprintf("localhost:%d", g.config.UIPort),
32 | Handler: new(router).configure(g.config),
33 | }
34 |
35 | log.Info("GitDB GUI will run at http://" + server.Addr)
36 | go func() {
37 | if err := server.ListenAndServe(); err != nil {
38 | log.Error(err.Error())
39 | }
40 | }()
41 |
42 | //listen for shutdown event
43 | go func() {
44 | <-g.shutdown
45 | if server != nil {
46 | log.Test("shutting down UI server")
47 | server.Shutdown(context.TODO())
48 | }
49 | return
50 | }()
51 | }
52 |
53 | type fileSystem struct {
54 | files map[string]string
55 | }
56 |
57 | func (e *fileSystem) embed(name, src string) {
58 | if e.files == nil {
59 | e.files = make(map[string]string)
60 | }
61 | e.files[name] = src
62 | }
63 |
64 | func (e *fileSystem) has(name string) bool {
65 | _, ok := e.files[name]
66 | return ok
67 | }
68 |
69 | func (e *fileSystem) get(name string) []byte {
70 | content := e.files[name]
71 | decoded, err := base64.StdEncoding.DecodeString(content)
72 | if err != nil {
73 | log.Error(err.Error())
74 | return []byte("")
75 | }
76 |
77 | return decoded
78 | }
79 |
80 | //router provides all the http handlers for the UI
81 | type router struct {
82 | datasets []*db.Dataset
83 | refreshAt time.Time
84 | }
85 |
86 | func (u *router) configure(cfg Config) *mux.Router {
87 | router := mux.NewRouter()
88 | for path, handler := range u.getEndpoints() {
89 | router.HandleFunc(path, handler)
90 | }
91 |
92 | //refresh dataset after 1 minute
93 | router.Use(func(h http.Handler) http.Handler {
94 | if u.refreshAt.IsZero() || u.refreshAt.Before(time.Now()) {
95 | u.datasets = db.LoadDatasets(filepath.Join(cfg.DBPath, "data"), cfg.EncryptionKey)
96 | u.refreshAt = time.Now().Add(time.Second * 10)
97 | }
98 |
99 | return h
100 | })
101 |
102 | return router
103 | }
104 |
105 | //getEndpoints maps a path to a http handler
106 | func (u *router) getEndpoints() map[string]http.HandlerFunc {
107 | return map[string]http.HandlerFunc{
108 | "/css/app.css": u.appCSS,
109 | "/js/app.js": u.appJS,
110 | "/": u.overview,
111 | "/errors/{dataset}": u.viewErrors,
112 | "/list/{dataset}": u.list,
113 | "/view/{dataset}": u.view,
114 | "/view/{dataset}/b{b}/r{r}": u.view,
115 | }
116 | }
117 |
118 | func (u *router) appCSS(w http.ResponseWriter, r *http.Request) {
119 | src := readView("static/css/app.css")
120 | w.Header().Set("Content-Type", "text/css")
121 | w.Write(src)
122 | }
123 |
124 | func (u *router) appJS(w http.ResponseWriter, r *http.Request) {
125 | src := readView("static/js/app.js")
126 | w.Header().Set("Content-Type", "text/javascript")
127 | w.Write(src)
128 | }
129 |
130 | func (u *router) overview(w http.ResponseWriter, r *http.Request) {
131 | viewModel := &overviewViewModel{}
132 | viewModel.Title = "Overview"
133 | viewModel.DataSets = u.datasets
134 |
135 | render(w, viewModel, "static/index.html", "static/sidebar.html")
136 | }
137 |
138 | func (u *router) list(w http.ResponseWriter, r *http.Request) {
139 | vars := mux.Vars(r)
140 | viewDs := vars["dataset"]
141 |
142 | dataset := u.findDataset(viewDs)
143 | if dataset == nil {
144 | w.Write([]byte("Dataset (" + viewDs + ") does not exist"))
145 | return
146 | }
147 |
148 | block := dataset.Block(0)
149 | table := tablulate(block)
150 | viewModel := &listDataSetViewModel{DataSet: dataset, Table: table}
151 | viewModel.DataSets = u.datasets
152 |
153 | render(w, viewModel, "static/list.html", "static/sidebar.html")
154 | }
155 |
156 | func (u *router) view(w http.ResponseWriter, r *http.Request) {
157 | vars := mux.Vars(r)
158 | viewDs := vars["dataset"]
159 |
160 | dataset := u.findDataset(viewDs)
161 | if dataset == nil {
162 | w.Write([]byte("Dataset (" + viewDs + ") does not exist"))
163 | return
164 | }
165 |
166 | viewModel := &viewDataSetViewModel{
167 | DataSet: dataset,
168 | Content: "No record found",
169 | Pager: &pager{totalBlocks: dataset.BlockCount()},
170 | }
171 | viewModel.DataSets = u.datasets
172 | if vars["b"] != "" && vars["r"] != "" {
173 | viewModel.Pager.set(vars["b"], vars["r"])
174 | }
175 | block := dataset.Block(viewModel.Pager.blockPage)
176 | viewModel.Block = block
177 | viewModel.Pager.totalRecords = block.RecordCount()
178 | if viewModel.Pager.totalRecords > viewModel.Pager.recordPage {
179 | viewModel.Content = block.Record(viewModel.Pager.recordPage).JSON()
180 | }
181 |
182 | render(w, viewModel, "static/view.html", "static/sidebar.html")
183 | }
184 |
185 | func (u *router) viewErrors(w http.ResponseWriter, r *http.Request) {
186 | vars := mux.Vars(r)
187 | viewDs := vars["dataset"]
188 |
189 | dataset := u.findDataset(viewDs)
190 | if dataset == nil {
191 | w.Write([]byte("Dataset (" + viewDs + ") does not exist"))
192 | return
193 | }
194 | viewModel := &errorsViewModel{DataSet: dataset}
195 | viewModel.Title = "Errors"
196 | viewModel.DataSets = u.datasets
197 |
198 | render(w, viewModel, "static/errors.html", "static/sidebar.html")
199 | }
200 |
201 | func (u *router) findDataset(name string) *db.Dataset {
202 | for _, ds := range u.datasets {
203 | if ds.Name() == name {
204 | return ds
205 | }
206 | }
207 | return nil
208 | }
209 |
210 | func render(w http.ResponseWriter, data interface{}, templates ...string) {
211 |
212 | parseFiles := false
213 | for _, template := range templates {
214 | if !getFs().has(template) {
215 | parseFiles = true
216 | }
217 | }
218 |
219 | var t *template.Template
220 | var err error
221 | if parseFiles {
222 | t, err = template.ParseFiles(templates...)
223 | if err != nil {
224 | log.Error(err.Error())
225 | }
226 | } else {
227 | t = template.New("overview")
228 | for _, template := range templates {
229 | log.Test("Reading EMBEDDED file - " + template)
230 | t, err = t.Parse(string(getFs().get(template)))
231 | if err != nil {
232 | log.Error(err.Error())
233 | }
234 | }
235 | }
236 |
237 | t.Execute(w, data)
238 | }
239 |
240 | func readView(fileName string) []byte {
241 | if getFs().has(fileName) {
242 | return getFs().get(fileName)
243 | }
244 |
245 | data, err := ioutil.ReadFile(fileName)
246 | if err != nil {
247 | log.Error(err.Error())
248 | return []byte("")
249 | }
250 |
251 | return data
252 | }
253 |
--------------------------------------------------------------------------------
/ui_static.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 | // Code generated by gitdb embed-ui on Tue, 07 Apr 2020 14:46:09 BST; DO NOT EDIT.
3 |
4 | func init() {
5 | //Embed Files
6 |
7 | getFs().embed("static/css/app.css", "Ym9keSB7cGFkZGluZzogMDttYXJnaW46IDA7Zm9udC1mYW1pbHk6IEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWY7fWRpdiB7Ym94LXNpemluZzogYm9yZGVyLWJveDt9aDEge3BhZGRpbmc6IDA7bWFyZ2luOiAwO21hcmdpbi1ib3R0b206IDMwcHg7fWgxIGEge3RleHQtZGVjb3JhdGlvbjogbm9uZTtjb2xvcjogZGFya3NlYWdyZWVuO30uc2lkZWJhciB7ZmxvYXQ6IGxlZnQ7d2lkdGg6IDIwJTtoZWlnaHQ6IDgwMHB4O2JhY2tncm91bmQtY29sb3I6ICNlZWU7Ym9yZGVyLXJpZ2h0OiAxcHggc29saWQgI2RkZDtwYWRkaW5nOiAxMHB4O30uY29udGVudCB7cGFkZGluZzogMzBweDtwYWRkaW5nLXRvcDogMTBweDtmbG9hdDogbGVmdDt3aWR0aDogODAlO2hlaWdodDogODAwcHg7fS5uYXYge2xpc3Qtc3R5bGU6IG5vbmU7bWFyZ2luOiAwO3BhZGRpbmc6IDB9Lm5hdiBsaSB7Y29sb3I6ICMwMDA7fS5uYXYgYSB7Y29sb3I6ICMwMDA7dGV4dC1kZWNvcmF0aW9uOiBub25lO2Rpc3BsYXk6IGJsb2NrO3BhZGRpbmctdG9wOiAxMHB4O3BhZGRpbmctYm90dG9tOiA1cHg7cGFkZGluZy1sZWZ0OiA1cHg7Ym9yZGVyLWJvdHRvbTogMXB4IHNvbGlkICNkZGQ7fS5uYXYgYTpob3ZlciB7YmFja2dyb3VuZC1jb2xvcjogI2RkZDt9dGFibGUgdHI6aG92ZXIgdGQge2N1cnNvcjogcG9pbnRlcjtiYWNrZ3JvdW5kLWNvbG9yOiAjY2NjO310YWJsZSB0aCB7YmFja2dyb3VuZC1jb2xvcjogZGFya3NlYWdyZWVuO2NvbG9yOiAjZmZmO3RleHQtYWxpZ246IGxlZnQ7fXRhYmxlIHt3aWR0aDogMTAwJTsvKiBib3JkZXI6IDFweCBzb2xpZCAjMDAwOyAqL2JvcmRlci1zcGFjaW5nOiAwcHg7fXRhYmxlIHRkLHRhYmxlIHRoIHtwYWRkaW5nOiAxMHB4O2JvcmRlci1ib3R0b206IDFweCBzb2xpZCAjZGRkO31wcmUge2JhY2tncm91bmQtY29sb3I6ICMyMjI7Y29sb3I6ICNmZmY7cGFkZGluZzogMTBweDtmb250LXNpemU6IDE0cHg7d2lkdGg6IDgwMHB4O292ZXJmbG93OiBoaWRkZW47fXRleHRhcmVhIHtkaXNwbGF5OiBibG9jazt9Lmxpc3RXaW5kb3cge3dpZHRoOiAxMDAlO292ZXJmbG93LXg6IHNjcm9sbDt9")
8 |
9 | getFs().embed("static/errors.html", "PGh0bWw+PGhlYWQ+PC9oZWFkPjxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iL2Nzcy9hcHAuY3NzIj48Ym9keT57e3RlbXBsYXRlICJzaWRlYmFyIiAkfX08ZGl2IGNsYXNzPSJjb250ZW50Ij48aDE+e3suVGl0bGV9fTwvaDE+e3tpZiAuRGF0YVNldC5CYWRCbG9ja3N9fTxoMj5CYWQgQmxvY2tzPC9oMj48dWw+e3tyYW5nZSAka2V5LCAkdmFsdWUgOj0gLkRhdGFTZXQuQmFkQmxvY2tzfX08bGk+PGEgaHJlZj0iL2VkaXQve3sgJHZhbHVlIH19Ij57eyAkdmFsdWUgfX08L2E+PC9saT57e2VuZH19PC91bD57e2VuZH19IHt7aWYgLkRhdGFTZXQuQmFkUmVjb3Jkc319PGgyPkJhZCBSZWNvcmRzPC9oMj48dWw+e3tyYW5nZSAka2V5LCAkdmFsdWUgOj0gLkRhdGFTZXQuQmFkUmVjb3Jkc319PGxpPjxhIGhyZWY9IiMiPnt7ICR2YWx1ZSB9fTwvYT48L2xpPnt7ZW5kfX08L3VsPnt7ZW5kfX08L2Rpdj48L2JvZHk+PC9odG1sPg==")
10 |
11 | getFs().embed("static/index.html", "PGh0bWw+PGhlYWQ+PC9oZWFkPjxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iL2Nzcy9hcHAuY3NzIj48c2NyaXB0IHNyYz0iL2pzL2FwcC5qcyI+PC9zY3JpcHQ+PGJvZHk+e3t0ZW1wbGF0ZSAic2lkZWJhciIgJH19PGRpdiBjbGFzcz0iY29udGVudCI+PGgxPnt7LlRpdGxlfX08L2gxPjx0YWJsZT48dHI+PHRoPkRhdGFzZXQ8L3RoPjx0aD5Oby4gb2YgYmxvY2tzPC90aD48dGg+Tm8uIG9mIHJlY29yZHM8L3RoPjx0aD5TaXplPC90aD48dGg+RXJyb3JzPC90aD48dGg+SW5kZXhlczwvdGg+PHRoPkxhc3QgTW9kaWZpZWQ8L3RoPjwvdHI+e3tyYW5nZSAka2V5LCAkdmFsdWUgOj0gLkRhdGFTZXRzfX08dHIgY2xhc3M9ImRhdGFzZXRSb3ciIGRhdGEtdmlldz0iL2xpc3Qve3sgJHZhbHVlLk5hbWUgfX0iPjx0ZD57eyAkdmFsdWUuTmFtZSB9fTwvdGQ+PHRkPnt7ICR2YWx1ZS5CbG9ja0NvdW50IH19PC90ZD48dGQ+e3sgJHZhbHVlLlJlY29yZENvdW50IH19PC90ZD48dGQ+e3sgJHZhbHVlLkh1bWFuU2l6ZSB9fTwvdGQ+PHRkPjxhIGhyZWY9Ii9lcnJvcnMve3sgJHZhbHVlLk5hbWUgfX0iPnt7ICR2YWx1ZS5CYWRCbG9ja3NDb3VudCB9fSBibG9jayhzKSAvIHt7ICR2YWx1ZS5CYWRSZWNvcmRzQ291bnQgfX0gcmVjb3JkKHMpPC9hPjwvdGQ+PHRkPjx1bD57e3JhbmdlICRpbmRleE5hbWUgOj0gJHZhbHVlLkluZGV4ZXN9fTxsaT57eyAkaW5kZXhOYW1lIH19PC9saT57e2VuZH19PC91bD48L3RkPjx0ZD57eyAkdmFsdWUuTGFzdE1vZGlmaWVkRGF0ZSB9fTwvdGQ+PC90cj57e2VuZH19PC90YWJsZT48L2Rpdj48L2JvZHk+PC9odG1sPg==")
12 |
13 | getFs().embed("static/js/app.js", "d2luZG93LmFkZEV2ZW50TGlzdGVuZXIoJ2xvYWQnLCAoZXZlbnQpID0+IHttYWtlRGF0YXNldFJvd3NDbGlja2FibGUoKTttYWtlUmVjb3JkUm93c0NsaWNrYWJsZSgpO30pO2Z1bmN0aW9uIG1ha2VEYXRhc2V0Um93c0NsaWNrYWJsZSgpIHtkb2N1bWVudC5xdWVyeVNlbGVjdG9yQWxsKCcuZGF0YXNldFJvdycpLmZvckVhY2gocm93ID0+IHtyb3cuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCBldmVudCA9PiB7d2luZG93LmxvY2F0aW9uID0gcm93LmRhdGFzZXQudmlld30pO30pfWZ1bmN0aW9uIG1ha2VSZWNvcmRSb3dzQ2xpY2thYmxlKCkge2RvY3VtZW50LnF1ZXJ5U2VsZWN0b3JBbGwoJy5yZWNvcmRSb3cnKS5mb3JFYWNoKHJvdyA9PiB7cm93LmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgZXZlbnQgPT4ge3dpbmRvdy5sb2NhdGlvbiA9IHJvdy5kYXRhc2V0LnZpZXd9KTt9KX0=")
14 |
15 | getFs().embed("static/list.html", "PGh0bWw+PGhlYWQ+PC9oZWFkPjxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iL2Nzcy9hcHAuY3NzIj48c2NyaXB0IHNyYz0iL2pzL2FwcC5qcyI+PC9zY3JpcHQ+PGJvZHk+e3t0ZW1wbGF0ZSAic2lkZWJhciIgJH19PGRpdiBjbGFzcz0iY29udGVudCI+PGgxPnt7LkRhdGFTZXQuTmFtZX19PC9oMT48ZGl2PjxzcGFuPnt7LkRhdGFTZXQuQmxvY2tDb3VudH19IGJsb2Nrczwvc3Bhbj4gPHNwYW4+e3suRGF0YVNldC5IdW1hblNpemV9fTwvc3Bhbj48L2Rpdj48ZGl2IGNsYXNzPSJsaXN0V2luZG93Ij48dGFibGU+PHRyPnt7cmFuZ2UgJGtleSwgJHZhbHVlIDo9IC5UYWJsZS5IZWFkZXJzfX08dGg+e3sgJHZhbHVlIH19PC90aD57e2VuZH19PC90cj57e3JhbmdlICRrZXksICR2YWx1ZSA6PSAuVGFibGUuUm93c319PHRyIGNsYXNzPSJyZWNvcmRSb3ciIGRhdGEtdmlldz0iL3ZpZXcve3skLkRhdGFTZXQuTmFtZX19L2IwL3J7eyAka2V5IH19Ij57e3JhbmdlICRrLCAkdiA6PSAkdmFsdWV9fSB7e2lmIGVxICRrIDB9fTx0ZD57eyAkdiB9fTwvdGQ+e3tlbHNlfX08dGQ+e3sgJHYgfX08L3RkPnt7ZW5kfX0ge3tlbmR9fTx0cj57e2VuZH19PC90YWJsZT48L2Rpdj48L2Rpdj48L2JvZHk+PC9odG1sPg==")
16 |
17 | getFs().embed("static/sidebar.html", "e3tkZWZpbmUgInNpZGViYXIifX08ZGl2IGNsYXNzPSJzaWRlYmFyIj48aDE+PGEgaHJlZj0iLyI+R2l0REI8L2E+PC9oMT48c3Ryb25nPkRhdGEgU2V0czwvc3Ryb25nPjx1bCBjbGFzcz0ibmF2Ij57e3JhbmdlICRrZXksICR2YWx1ZSA6PSAuRGF0YVNldHN9fTxsaT48YSBocmVmPSIvbGlzdC97eyAkdmFsdWUuTmFtZSB9fSI+e3sgJHZhbHVlLk5hbWUgfX08L2E+PC9saT57e2VuZH19PC91bD48L2Rpdj57e2VuZH19")
18 |
19 | getFs().embed("static/view.html", "PGh0bWw+PGhlYWQ+PC9oZWFkPjxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iL2Nzcy9hcHAuY3NzIj48Ym9keT57e3RlbXBsYXRlICJzaWRlYmFyIiAkfX08ZGl2IGNsYXNzPSJjb250ZW50Ij48aDE+e3suRGF0YVNldC5OYW1lfX08L2gxPjxkaXY+PHNwYW4+e3suRGF0YVNldC5CbG9ja0NvdW50fX0gYmxvY2tzPC9zcGFuPiA8c3Bhbj57ey5CbG9jay5IdW1hblNpemV9fS97ey5EYXRhU2V0Lkh1bWFuU2l6ZX19PC9zcGFuPjwvZGl2PjxhIGhyZWY9Ii92aWV3L3t7LkRhdGFTZXQuTmFtZX19L3t7LlBhZ2VyLlByZXZCbG9ja1VSSX19Ij5QcmV2IEJsb2NrPC9hPiB8IDxhIGhyZWY9Ii92aWV3L3t7LkRhdGFTZXQuTmFtZX19L3t7LlBhZ2VyLk5leHRCbG9ja1VSSX19Ij5OZXh0IEJsb2NrPC9hPjxwcmU+e3suQ29udGVudH19PC9wcmU+PGEgaHJlZj0iL3ZpZXcve3suRGF0YVNldC5OYW1lfX0ve3suUGFnZXIuUHJldlJlY29yZFVSSX19Ij5QcmV2IFJlY29yZDwvYT4gfCA8YSBocmVmPSIvdmlldy97ey5EYXRhU2V0Lk5hbWV9fS97ey5QYWdlci5OZXh0UmVjb3JkVVJJfX0iPk5leHQgUmVjb3JkPC9hPjwvZGl2PjwvYm9keT48L2h0bWw+")
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/ui_test.go:
--------------------------------------------------------------------------------
1 | package gitdb_test
2 |
3 | import (
4 | "net/http"
5 | "testing"
6 | )
7 |
8 | func TestServer(t *testing.T) {
9 | cfg := getConfig()
10 | cfg.EnableUI = true
11 | teardown := setup(t, cfg)
12 |
13 | insert(getTestMessageWithId(1), false)
14 |
15 | //fire off some requests
16 | client := http.DefaultClient
17 | requests := []*http.Request{
18 | request(http.MethodGet, "http://localhost:4120/css/app.css"),
19 | request(http.MethodGet, "http://localhost:4120/js/app.js"),
20 | request(http.MethodGet, "http://localhost:4120/"),
21 | request(http.MethodGet, "http://localhost:4120/errors/Message"),
22 | request(http.MethodGet, "http://localhost:4120/list/Message"),
23 | request(http.MethodGet, "http://localhost:4120/view/Message"),
24 | request(http.MethodGet, "http://localhost:4120/view/Message/b0/r0"),
25 | }
26 |
27 | for _, req := range requests {
28 | t.Logf("Testing %s", req.URL.String())
29 | resp, err := client.Do(req)
30 | if err != nil {
31 | t.Errorf("GitDB UI Server request failed: %s", err)
32 | }
33 |
34 | //todo use golden files to check response
35 | // b, _ := ioutil.ReadAll(resp.Body)
36 | resp.Body.Close()
37 | // t.Log(string(b))
38 | }
39 |
40 | teardown(t)
41 | }
42 |
43 | func request(method, url string) *http.Request {
44 | req, _ := http.NewRequest(method, url, nil)
45 | return req
46 | }
47 |
--------------------------------------------------------------------------------
/ui_view.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "fmt"
5 | "sort"
6 | "strconv"
7 |
8 | "github.com/bouggo/log"
9 | "github.com/gogitdb/gitdb/v2/internal/db"
10 | )
11 |
12 | //table represents a tabular view
13 | type table struct {
14 | Headers []string
15 | Rows [][]string
16 | }
17 |
18 | //tablulate returns a tabular representation of a Block
19 | func tablulate(b *db.Block) *table {
20 | t := &table{}
21 | var jsonMap map[string]interface{}
22 |
23 | for i, record := range b.Records() {
24 | if err := record.Hydrate(&jsonMap); err != nil {
25 | log.Error(err.Error())
26 | continue
27 | }
28 |
29 | var row []string
30 | if i == 0 {
31 | t.Headers = sortHeaderFields(jsonMap)
32 | }
33 | for _, key := range t.Headers {
34 | val := fmt.Sprintf("%v", jsonMap[key])
35 | if len(val) > 40 {
36 | val = val[0:40]
37 | }
38 | row = append(row, val)
39 | }
40 |
41 | t.Rows = append(t.Rows, row)
42 | }
43 |
44 | return t
45 | }
46 |
47 | func sortHeaderFields(recMap map[string]interface{}) []string {
48 | // To store the keys in slice in sorted order
49 | var keys []string
50 | for k := range recMap {
51 | keys = append(keys, k)
52 | }
53 | sort.Strings(keys)
54 | return keys
55 | }
56 |
57 | //pager is used to paginate records for the UI
58 | type pager struct {
59 | blockPage int
60 | recordPage int
61 | totalBlocks int
62 | totalRecords int
63 | }
64 |
65 | //set page of Pager
66 | func (p *pager) set(blockFlag string, recordFlag string) {
67 | log.Test("Setting pager: " + blockFlag + "," + recordFlag)
68 | p.blockPage, _ = strconv.Atoi(blockFlag)
69 | p.recordPage, _ = strconv.Atoi(recordFlag)
70 | }
71 |
72 | //NextRecordURI returns the uri for the next record
73 | func (p *pager) NextRecordURI() string {
74 | recordPage := p.recordPage
75 | if p.recordPage < p.totalRecords-1 {
76 | recordPage = p.recordPage + 1
77 | }
78 |
79 | return fmt.Sprintf("b%d/r%d", p.blockPage, recordPage)
80 | }
81 |
82 | //PrevRecordURI returns uri for the previous record
83 | func (p *pager) PrevRecordURI() string {
84 | recordPage := p.recordPage
85 | if p.recordPage > 0 {
86 | recordPage = p.recordPage - 1
87 | }
88 |
89 | return fmt.Sprintf("b%d/r%d", p.blockPage, recordPage)
90 | }
91 |
92 | //NextBlockURI returns uri for the next block
93 | func (p *pager) NextBlockURI() string {
94 | blockPage := p.blockPage
95 | if p.blockPage < p.totalBlocks-1 {
96 | blockPage = p.blockPage + 1
97 | }
98 |
99 | return fmt.Sprintf("b%d/r%d", blockPage, 0)
100 | }
101 |
102 | //PrevBlockURI returns uri for the previous block
103 | func (p *pager) PrevBlockURI() string {
104 | blockPage := p.blockPage
105 | if p.blockPage > 0 {
106 | blockPage = p.blockPage - 1
107 | }
108 |
109 | return fmt.Sprintf("b%d/r%d", blockPage, 0)
110 | }
111 |
--------------------------------------------------------------------------------
/ui_viewmodels.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import "github.com/gogitdb/gitdb/v2/internal/db"
4 |
5 | type baseViewModel struct {
6 | Title string
7 | DataSets []*db.Dataset
8 | }
9 |
10 | type overviewViewModel struct {
11 | baseViewModel
12 | }
13 |
14 | type viewDataSetViewModel struct {
15 | baseViewModel
16 | DataSet *db.Dataset
17 | Block *db.Block
18 | Pager *pager
19 | Content string
20 | }
21 |
22 | type listDataSetViewModel struct {
23 | baseViewModel
24 | DataSet *db.Dataset
25 | Table *table
26 | }
27 |
28 | type errorsViewModel struct {
29 | baseViewModel
30 | DataSet *db.Dataset
31 | }
32 |
--------------------------------------------------------------------------------
/upload.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "os"
8 | "path"
9 | "path/filepath"
10 | "strings"
11 | )
12 |
13 | const uploadDataset = "Bucket"
14 |
15 | var validExtensions = map[string]bool{
16 | ".html": true,
17 | ".pdf": true,
18 | ".doc": true,
19 | ".xlsx": true,
20 | ".csv": true,
21 | ".jpg": true,
22 | ".gif": true,
23 | ".png": true,
24 | ".svg": true,
25 | ".json": true,
26 | ".yaml": true,
27 | ".yml": true,
28 | ".md": true,
29 | }
30 |
31 | //UploadModel represents a file upload
32 | type UploadModel struct {
33 | Bucket string
34 | File string
35 | Path string
36 | UploadedBy string
37 | TimeStampedModel
38 | }
39 |
40 | //GetSchema implements Model.GetSchema
41 | func (u *UploadModel) GetSchema() *Schema {
42 | return newSchema(
43 | uploadDataset,
44 | // AutoBlock(u.db.dbDir(), name, BlockByCount, 1000),
45 | u.Bucket,
46 | u.File,
47 | make(map[string]interface{}),
48 | )
49 | }
50 |
51 | //Validate implements Model.Validate
52 | func (u *UploadModel) Validate() error {
53 | //todo
54 | return nil
55 | }
56 |
57 | func (U *UploadModel) ShouldEncrypt() bool { return false }
58 |
59 | //Upload provides API for managing file uploads
60 | type Upload struct {
61 | db *gitdb
62 | model *UploadModel
63 | }
64 |
65 | //Get returns an upload by id
66 | func (u *Upload) Get(id string, result *UploadModel) error {
67 | return u.db.Get(id, result)
68 | }
69 |
70 | //Delete an upload by id
71 | func (u *Upload) Delete(id string) error {
72 | var data UploadModel
73 | if err := u.Get(id, &data); err != nil {
74 | return err
75 | }
76 |
77 | err := u.db.Delete(id)
78 | if err == nil {
79 | err = os.Remove(data.Path)
80 | }
81 |
82 | return err
83 | }
84 |
85 | //New uploads specified file into bucket
86 | func (u *Upload) New(bucket, file string) error {
87 | err := u.db.Exists(u.id(bucket, file))
88 | if err == nil {
89 | return errors.New("file already exists")
90 | }
91 |
92 | return u.upload(bucket, file)
93 | }
94 |
95 | //Replace overrides a specified file in bucket
96 | func (u *Upload) Replace(bucket, file string) error {
97 | err := u.db.Exists(u.id(bucket, file))
98 | if err != nil {
99 | return err
100 | }
101 |
102 | return u.upload(bucket, file)
103 | }
104 |
105 | func (u *Upload) id(bucket, file string) string {
106 | return uploadDataset + "/" + bucket + "/" + u.cleanFileName(file)
107 | }
108 |
109 | func (u *Upload) cleanFileName(filename string) string {
110 | //todo a better func to clean up filenames
111 | filename = strings.ReplaceAll(path.Clean(filename), "/", "-")
112 | filename = strings.ReplaceAll(filename, " ", "-")
113 | return filename
114 | }
115 |
116 | func (u *Upload) upload(bucket, file string) error {
117 | var src *os.File
118 | var dst *os.File
119 | var err error
120 |
121 | if src, err = os.Open(file); err != nil {
122 | return err
123 | }
124 |
125 | ext := filepath.Ext(file)
126 | if _, ok := validExtensions[ext]; !ok {
127 | return fmt.Errorf("%s files are not allowed", ext)
128 | }
129 |
130 | filename := u.cleanFileName(file)
131 | uploadPath := filepath.Join(u.db.dbDir(), uploadDataset, bucket, filename)
132 | fmt.Println(uploadPath)
133 | if err = os.MkdirAll(path.Dir(uploadPath), os.ModePerm); err != nil {
134 | return err
135 | }
136 |
137 | if dst, err = os.Create(uploadPath); err != nil {
138 | return err
139 | }
140 | if _, err = io.Copy(dst, src); err != nil {
141 | return err
142 | }
143 |
144 | //neutralise file
145 | if err = dst.Chmod(0640); err != nil {
146 | return err
147 | }
148 |
149 | //Uploads/b0/bucket-file.jpg
150 | m := &UploadModel{
151 | Bucket: bucket,
152 | File: filename,
153 | Path: uploadPath,
154 | UploadedBy: u.db.config.User.String(),
155 | }
156 | return u.db.Insert(m)
157 | }
158 |
159 | func (g *gitdb) Upload() *Upload {
160 | g.RegisterModel(uploadDataset, &UploadModel{})
161 | return &Upload{db: g}
162 | }
163 |
--------------------------------------------------------------------------------
/upload_test.go:
--------------------------------------------------------------------------------
1 | package gitdb_test
2 |
3 | import "testing"
4 |
5 | func TestUploadNew(t *testing.T) {
6 | teardown := setup(t, getConfig())
7 | defer teardown(t)
8 |
9 | err := testDb.Upload().New("creds", "./README.md")
10 | if err != nil {
11 | t.Errorf("Upload.New() failed: %s", err)
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/user.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | //User represents the user currently connected to the database
4 | //and will be used to identify who made changes to it
5 | type User struct {
6 | Name string
7 | Email string
8 | }
9 |
10 | //AuthorName return commit author git style
11 | func (u *User) AuthorName() string {
12 | return u.Name + " <" + u.Email + ">"
13 | }
14 |
15 | //String is an alias for AuthorName
16 | func (u *User) String() string {
17 | return u.AuthorName()
18 | }
19 |
20 | //NewUser constructs a *DbUser
21 | func NewUser(name string, email string) *User {
22 | return &User{Name: name, Email: email}
23 | }
24 |
25 | //SetUser sets the user connection
26 | func (g *gitdb) SetUser(user *User) error {
27 | g.config.User = user
28 | return nil
29 | }
30 |
--------------------------------------------------------------------------------
/user_test.go:
--------------------------------------------------------------------------------
1 | package gitdb_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/gogitdb/gitdb/v2"
7 | )
8 |
9 | func TestNewUser(t *testing.T) {
10 | user := gitdb.NewUser("test", "tester@gitdb.io")
11 | want := "test "
12 | got := user.AuthorName()
13 | if want != got {
14 | t.Errorf("want: %s, got: %s", want, got)
15 | }
16 |
17 | got = user.String()
18 | if want != got {
19 | t.Errorf("want: %s, got: %s", want, got)
20 | }
21 | }
22 |
23 | func TestSetUser(t *testing.T) {
24 | teardown := setup(t, nil)
25 | defer teardown(t)
26 |
27 | user := gitdb.NewUser("test", "tester@gitdb.io")
28 | if err := testDb.SetUser(user); err != nil {
29 | t.Errorf("testDb.SetUser failed: %s", err)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/write.go:
--------------------------------------------------------------------------------
1 | package gitdb
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "io/ioutil"
8 | "os"
9 |
10 | "github.com/bouggo/log"
11 | "github.com/gogitdb/gitdb/v2/internal/crypto"
12 | "github.com/gogitdb/gitdb/v2/internal/db"
13 | )
14 |
15 | func (g *gitdb) Insert(mo Model) error {
16 | m := wrap(mo)
17 |
18 | if err := m.Validate(); err != nil {
19 | return err
20 | }
21 |
22 | if err := m.BeforeInsert(); err != nil {
23 | return fmt.Errorf("Model.BeforeInsert failed: %s", err)
24 | }
25 |
26 | if err := m.GetSchema().Validate(); err != nil {
27 | return err
28 | }
29 |
30 | return g.insert(m)
31 | }
32 |
33 | func (g *gitdb) InsertMany(models []Model) error {
34 | tx := g.StartTransaction("InsertMany")
35 | for _, model := range models {
36 | //create a new variable to pass to function to avoid
37 | //passing pointer which will end up inserting the same
38 | //model multiple times
39 | m := model
40 | f := func() error { return g.Insert(m) }
41 | tx.AddOperation(f)
42 | }
43 | return tx.Commit()
44 | }
45 |
46 | func (g *gitdb) insert(m Model) error {
47 | if !g.isRegistered(m.GetSchema().dataset) {
48 | return ErrInvalidDataset
49 | }
50 |
51 | if _, err := os.Stat(g.fullPath(m)); err != nil {
52 | err := os.MkdirAll(g.fullPath(m), 0755)
53 | if err != nil {
54 | return fmt.Errorf("failed to make dir %s: %w", g.fullPath(m), err)
55 | }
56 | }
57 |
58 | schema := m.GetSchema()
59 | blockFilePath := g.blockFilePath(schema.name(), schema.block)
60 | dataBlock, err := g.loadBlock(blockFilePath)
61 | if err != nil {
62 | return err
63 | }
64 |
65 | log.Test(fmt.Sprintf("Size of block before write - %d", dataBlock.Len()))
66 |
67 | //...append new record to block
68 | newRecordBytes, err := json.Marshal(m)
69 | if err != nil {
70 | return err
71 | }
72 |
73 | mID := ID(m)
74 |
75 | //construct a commit message
76 | commitMsg := "Inserting " + mID
77 | if _, err := dataBlock.Get(mID); err == nil {
78 | commitMsg = "Updating " + mID
79 | }
80 |
81 | newRecordStr := string(newRecordBytes)
82 | //encrypt data if need be
83 | if m.ShouldEncrypt() {
84 | newRecordStr = crypto.Encrypt(g.config.EncryptionKey, newRecordStr)
85 | }
86 |
87 | dataBlock.Add(mID, newRecordStr)
88 |
89 | g.events <- newWriteBeforeEvent("...", mID)
90 | if err := g.writeBlock(blockFilePath, dataBlock); err != nil {
91 | return err
92 | }
93 |
94 | log.Info(fmt.Sprintf("autoCommit: %v", g.autoCommit))
95 |
96 | g.commit.Add(1)
97 | g.events <- newWriteEvent(commitMsg, blockFilePath, g.autoCommit)
98 | log.Test("sent write event to loop")
99 | g.updateIndexes(dataBlock)
100 |
101 | //block here until write has been committed
102 | g.waitForCommit()
103 |
104 | return nil
105 | }
106 |
107 | func (g *gitdb) waitForCommit() {
108 | if g.autoCommit {
109 | log.Test("waiting for gitdb to commit changes")
110 | g.commit.Wait()
111 | }
112 | }
113 |
114 | func (g *gitdb) writeBlock(blockFile string, block *db.Block) error {
115 | g.writeMu.Lock()
116 | defer g.writeMu.Unlock()
117 |
118 | blockBytes, fmtErr := json.MarshalIndent(block, "", "\t")
119 | if fmtErr != nil {
120 | return fmtErr
121 | }
122 |
123 | //update cache
124 | if g.loadedBlocks != nil {
125 | g.loadedBlocks[blockFile] = block
126 | }
127 | return ioutil.WriteFile(blockFile, blockBytes, 0744)
128 | }
129 |
130 | func (g *gitdb) Delete(id string) error {
131 | return g.doDelete(id, false)
132 | }
133 |
134 | func (g *gitdb) DeleteOrFail(id string) error {
135 | return g.doDelete(id, true)
136 | }
137 |
138 | func (g *gitdb) doDelete(id string, failNotFound bool) error {
139 |
140 | dataset, block, _, err := ParseID(id)
141 | if err != nil {
142 | return err
143 | }
144 |
145 | blockFilePath := g.blockFilePath(dataset, block)
146 | err = g.delByID(id, blockFilePath, failNotFound)
147 |
148 | if err == nil {
149 | log.Test("sending delete event to loop")
150 | g.commit.Add(1)
151 | g.events <- newDeleteEvent(fmt.Sprintf("Deleting %s", id), blockFilePath, g.autoCommit)
152 | g.waitForCommit()
153 | }
154 |
155 | return err
156 | }
157 |
158 | func (g *gitdb) delByID(id string, blockFile string, failIfNotFound bool) error {
159 |
160 | if _, err := os.Stat(blockFile); err != nil {
161 | if failIfNotFound {
162 | return errors.New("Could not delete [" + id + "]: record does not exist")
163 | }
164 | return nil
165 | }
166 |
167 | dataBlock := db.LoadBlock(blockFile, g.config.EncryptionKey)
168 | if err := dataBlock.Delete(id); err != nil {
169 | if failIfNotFound {
170 | return errors.New("Could not delete [" + id + "]: record does not exist")
171 | }
172 | return nil
173 | }
174 |
175 | //write undeleted records back to block file
176 | return g.writeBlock(blockFile, dataBlock)
177 | }
178 |
--------------------------------------------------------------------------------
/write_test.go:
--------------------------------------------------------------------------------
1 | package gitdb_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/gogitdb/gitdb/v2"
7 | )
8 |
9 | func TestInsert(t *testing.T) {
10 | teardown := setup(t, nil)
11 | defer teardown(t)
12 | m := getTestMessage()
13 | err := insert(m, false)
14 | if err != nil {
15 | t.Errorf(err.Error())
16 | }
17 | }
18 |
19 | func TestInsertMany(t *testing.T) {
20 | teardown := setup(t, nil)
21 | defer teardown(t)
22 | defer testDb.Close()
23 | msgs := []gitdb.Model{}
24 | for i := 0; i < 10; i++ {
25 | m := getTestMessage()
26 | msgs = append(msgs, m)
27 | }
28 |
29 | err := testDb.InsertMany(msgs)
30 | if err != nil {
31 | t.Errorf(err.Error())
32 | }
33 | }
34 |
35 | func BenchmarkInsert(b *testing.B) {
36 | teardown := setup(b, nil)
37 | defer teardown(b)
38 | b.ReportAllocs()
39 |
40 | var m gitdb.Model
41 | for i := 0; i <= b.N; i++ {
42 | m = getTestMessage()
43 | err := insert(m, true)
44 | if err != nil {
45 | b.Errorf(err.Error())
46 | }
47 | }
48 | }
49 |
50 | func TestDelete(t *testing.T) {
51 | teardown := setup(t, nil)
52 | defer teardown(t)
53 |
54 | m := getTestMessageWithId(0)
55 | if err := insert(m, flagFakeRemote); err != nil {
56 | t.Errorf("Error: %s", err.Error())
57 | }
58 |
59 | if err := testDb.Delete(gitdb.ID(m)); err != nil {
60 | t.Errorf("Error: %s", err.Error())
61 | }
62 | }
63 |
64 | func TestDeleteOrFail(t *testing.T) {
65 | teardown := setup(t, nil)
66 | defer teardown(t)
67 |
68 | m := getTestMessageWithId(0)
69 | if err := insert(m, flagFakeRemote); err != nil {
70 | t.Errorf("Error: %s", err.Error())
71 | }
72 |
73 | if err := testDb.DeleteOrFail("non_existent_id"); err == nil {
74 | t.Errorf("Error: %s", err.Error())
75 | }
76 |
77 | if err := testDb.DeleteOrFail("Message/b0/1"); err == nil {
78 | t.Errorf("Error: %s", err.Error())
79 | }
80 | }
81 |
82 | func TestLockUnlock(t *testing.T) {
83 | teardown := setup(t, nil)
84 | defer teardown(t)
85 |
86 | m := getTestMessage()
87 | if err := testDb.Lock(m); err != nil {
88 | t.Errorf("testDb.Lock returned - %s", err)
89 | }
90 |
91 | if err := testDb.Unlock(m); err != nil {
92 | t.Errorf("testDb.Unlock returned - %s", err)
93 | }
94 | }
95 |
96 | func TestGetLockFileNames(t *testing.T) {
97 | m := getTestMessage()
98 | locks := m.GetLockFileNames()
99 | if len(locks) != 1 {
100 | t.Errorf("testMessage return %d lock files", len(locks))
101 | }
102 | }
103 |
--------------------------------------------------------------------------------