Skip to content

Commit 7d1fb7f

Browse files
committed
Import storage packages from c2FmZQ and re-license
* Import some internal packages from c2FmZQ/c2FmZQ/internal. * Decouple the logging code from c2FmZQ/c2FmZQ/internal/log. * Change the license to MIT.
0 parents  commit 7d1fb7f

18 files changed

+3960
-0
lines changed

.github/workflows/pr.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: PR
2+
on:
3+
- pull_request
4+
- push
5+
jobs:
6+
build-and-run-tests:
7+
name: Build & run tests
8+
if: github.ref_type == 'branch'
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v3
12+
- name: Set up Go
13+
uses: actions/setup-go@v4
14+
with:
15+
go-version-file: go.mod
16+
- name: Build
17+
run: go build ./...
18+
- name: Run go vet
19+
run: go vet ./...
20+
- name: Run go tests
21+
run: go test -v ./...

.github/workflows/release.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: release
2+
on:
3+
push:
4+
tags:
5+
- "v*.*"
6+
7+
jobs:
8+
build-and-run-tests:
9+
name: Build & run tests
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v3
13+
- name: Set up Go
14+
uses: actions/setup-go@v4
15+
with:
16+
go-version-file: go.mod
17+
- name: Build
18+
run: go build ./...
19+
- name: Run go vet
20+
run: go vet ./...
21+
- name: Run go tests
22+
run: go test -v ./...
23+
24+
create-release:
25+
name: Create release
26+
needs: build-and-run-tests
27+
runs-on: ubuntu-latest
28+
permissions:
29+
contents: write
30+
steps:
31+
- uses: actions/checkout@v3
32+
- name: Create release
33+
uses: softprops/action-gh-release@v1
34+
with:
35+
draft: false
36+
generate_release_notes: true

LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2021-2023 TTBT Enterprises LLC
4+
Copyright (c) 2021-2023 Robin Thellend <rthellend@rthellend.com>
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Storage
2+
3+
This go package is used to store data in encrypted files. It is designed for convenience and correctness, and may not be suitable for high volume or high throughput applications. It was refactored from internal [c2fmzq-server](https://github.com/c2FmZQ/c2FmZQ) code, and re-licenced, so that other projects can use it.
4+
5+
GO objects can be saved, loaded, and atomically updated.
6+
7+
```go
8+
mk, err := crypto.CreateMasterKey([]byte("<passphrase>"))
9+
if err != nil {
10+
panic(err)
11+
}
12+
store := storage.New("<data dir>", mk)
13+
14+
var obj1 Object
15+
// Populate obj1
16+
if err := store.SaveDataFile("<relative filename>", &obj1); err != nil {
17+
panic(err)
18+
}
19+
20+
var obj2 Object
21+
if err := store.ReadDataFile("<relative filename>", &obj2); err != nil {
22+
panic(err)
23+
}
24+
// obj1 and obj2 have the same value
25+
```
26+
27+
To update objects atomically:
28+
```go
29+
func foo() (retErr error) {
30+
var obj Object
31+
commit, err := store.OpenForUpdate("<relative filename>", &obj)
32+
if err != nil {
33+
panic(err)
34+
}
35+
defer commit(false, &retErr) // rollback unless first committed.
36+
// modify obj
37+
obj.Bar = X
38+
return commit(true, nil) // commit
39+
}
40+
```
41+
42+
Multiple objects can be updated atomically with `OpenManyForUpdate()`.
43+
44+
Developers can also use `OpenBlobRead()` and `OpenBlobWrite()` to read and write encrypted BLOBs with a streaming API.
45+

autocertcache/cache.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// MIT License
2+
//
3+
// Copyright (c) 2021-2023 TTBT Enterprises LLC
4+
// Copyright (c) 2021-2023 Robin Thellend <rthellend@rthellend.com>
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in all
14+
// copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
// SOFTWARE.
23+
24+
package autocertcache
25+
26+
import (
27+
"context"
28+
"encoding/base64"
29+
30+
"github.com/c2FmZQ/storage"
31+
"golang.org/x/crypto/acme/autocert"
32+
)
33+
34+
type cacheContent struct {
35+
Entries map[string]string `json:"entries"`
36+
}
37+
38+
var _ autocert.Cache = (*Cache)(nil)
39+
40+
// New returns a new Autocert Cache stored in fileName and encrypted with storage.
41+
func New(fileName string, storage *storage.Storage) *Cache {
42+
storage.CreateEmptyFile(fileName, cacheContent{})
43+
return &Cache{fileName, storage}
44+
}
45+
46+
// Cache implements autocert.Cache
47+
type Cache struct {
48+
fileName string
49+
storage *storage.Storage
50+
}
51+
52+
// Get returns a cached entry.
53+
func (c *Cache) Get(_ context.Context, key string) ([]byte, error) {
54+
c.storage.Logger().Debugf("Cache.Get(%q)", key)
55+
var cc cacheContent
56+
if err := c.storage.ReadDataFile(c.fileName, &cc); err != nil {
57+
return nil, err
58+
}
59+
if cc.Entries == nil {
60+
cc.Entries = make(map[string]string)
61+
}
62+
e, ok := cc.Entries[key]
63+
if !ok {
64+
c.storage.Logger().Debugf("Cache.Get(%q) NOT found.", key)
65+
return nil, autocert.ErrCacheMiss
66+
}
67+
c.storage.Logger().Debugf("Cache.Get(%q) found.", key)
68+
return base64.StdEncoding.DecodeString(e)
69+
}
70+
71+
// Put stores a cache entry.
72+
func (c *Cache) Put(_ context.Context, key string, data []byte) error {
73+
c.storage.Logger().Debugf("Cache.Put(%q, ...)", key)
74+
var cc cacheContent
75+
commit, err := c.storage.OpenForUpdate(c.fileName, &cc)
76+
if err != nil {
77+
return err
78+
}
79+
if cc.Entries == nil {
80+
cc.Entries = make(map[string]string)
81+
}
82+
cc.Entries[key] = base64.StdEncoding.EncodeToString(data)
83+
return commit(true, nil)
84+
}
85+
86+
// Delete deletes a cached entry.
87+
func (c *Cache) Delete(_ context.Context, key string) error {
88+
c.storage.Logger().Debugf("Cache.Delete(%q)", key)
89+
var cc cacheContent
90+
commit, err := c.storage.OpenForUpdate(c.fileName, &cc)
91+
if err != nil {
92+
return err
93+
}
94+
if cc.Entries == nil {
95+
cc.Entries = make(map[string]string)
96+
}
97+
delete(cc.Entries, key)
98+
return commit(true, nil)
99+
}

backup.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// MIT License
2+
//
3+
// Copyright (c) 2021-2023 TTBT Enterprises LLC
4+
// Copyright (c) 2021-2023 Robin Thellend <rthellend@rthellend.com>
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in all
14+
// copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
// SOFTWARE.
23+
24+
package storage
25+
26+
import (
27+
"errors"
28+
"fmt"
29+
"io"
30+
"os"
31+
"path/filepath"
32+
"time"
33+
)
34+
35+
func (s *Storage) createBackup(files []string) (*backup, error) {
36+
b := &backup{dir: s.dir, TS: time.Now(), Files: files}
37+
if err := b.backup(); err != nil {
38+
return nil, err
39+
}
40+
b.pending = filepath.Join("pending", fmt.Sprintf("%d", b.TS.UnixNano()))
41+
if err := s.SaveDataFile(b.pending, b); err != nil {
42+
return nil, err
43+
}
44+
return b, nil
45+
}
46+
47+
func (s *Storage) rollbackPendingOps() error {
48+
m, err := filepath.Glob(filepath.Join(s.dir, "pending", "*"))
49+
if err != nil {
50+
return err
51+
}
52+
for _, f := range m {
53+
rel, err := filepath.Rel(s.dir, f)
54+
if err != nil {
55+
return err
56+
}
57+
var b backup
58+
if err := s.ReadDataFile(rel, &b); err != nil {
59+
return err
60+
}
61+
b.dir = s.dir
62+
b.pending = rel
63+
// Make sure pending is this backup is really abandoned.
64+
time.Sleep(time.Until(b.TS.Add(5 * time.Second)))
65+
if err := b.restore(); err != nil {
66+
return err
67+
}
68+
s.Logger().Infof("Rolled back pending operation %d [%v]", b.TS.UnixNano(), b.Files)
69+
// The abandoned files were most likely locked.
70+
s.UnlockMany(b.Files)
71+
}
72+
return nil
73+
}
74+
75+
type backup struct {
76+
// The timestamp of the backup.
77+
TS time.Time `json:"ts"`
78+
// Relative file names.
79+
Files []string `json:"files"`
80+
81+
// The root of the data directory.
82+
dir string
83+
// The relative file name of the pending ops file.
84+
pending string
85+
}
86+
87+
func (b *backup) backup() error {
88+
ch := make(chan error)
89+
for _, f := range b.Files {
90+
go func(fn string) { ch <- copyFile(b.backupFileName(fn), fn) }(filepath.Join(b.dir, f))
91+
}
92+
var errList []error
93+
for _ = range b.Files {
94+
if err := <-ch; err != nil && !errors.Is(err, os.ErrNotExist) {
95+
errList = append(errList, err)
96+
}
97+
}
98+
if errList != nil {
99+
return fmt.Errorf("%w %v", errList[0], errList[1:])
100+
}
101+
return nil
102+
}
103+
104+
func (b *backup) restore() error {
105+
ch := make(chan error)
106+
for _, f := range b.Files {
107+
go func(fn string) { ch <- os.Rename(b.backupFileName(fn), fn) }(filepath.Join(b.dir, f))
108+
}
109+
var errList []error
110+
for _ = range b.Files {
111+
if err := <-ch; err != nil && !errors.Is(err, os.ErrNotExist) {
112+
errList = append(errList, err)
113+
}
114+
}
115+
if errList != nil {
116+
return fmt.Errorf("%w %v", errList[0], errList[1:])
117+
}
118+
if err := os.Remove(filepath.Join(b.dir, b.pending)); err != nil && !errors.Is(err, os.ErrNotExist) {
119+
return err
120+
}
121+
return nil
122+
}
123+
124+
func (b *backup) delete() error {
125+
ch := make(chan error)
126+
for _, f := range b.Files {
127+
go func(fn string) { ch <- os.Remove(b.backupFileName(fn)) }(filepath.Join(b.dir, f))
128+
}
129+
var errList []error
130+
for _ = range b.Files {
131+
if err := <-ch; err != nil && !errors.Is(err, os.ErrNotExist) {
132+
errList = append(errList, err)
133+
}
134+
}
135+
if errList != nil {
136+
return fmt.Errorf("%w %v", errList[0], errList[1:])
137+
}
138+
if err := os.Remove(filepath.Join(b.dir, b.pending)); err != nil && !errors.Is(err, os.ErrNotExist) {
139+
return err
140+
}
141+
return nil
142+
}
143+
144+
func (b *backup) backupFileName(f string) string {
145+
return fmt.Sprintf("%s.bck-%d", f, b.TS.UnixNano())
146+
}
147+
148+
func copyFile(dst, src string) error {
149+
if err := os.Link(src, dst); err == nil {
150+
return nil
151+
}
152+
in, err := os.Open(src)
153+
if err != nil {
154+
return err
155+
}
156+
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
157+
if err != nil {
158+
in.Close()
159+
return err
160+
}
161+
if _, err := io.Copy(out, in); err != nil {
162+
in.Close()
163+
out.Close()
164+
return err
165+
}
166+
if err := out.Close(); err != nil {
167+
in.Close()
168+
return err
169+
}
170+
return in.Close()
171+
}

0 commit comments

Comments
 (0)