Skip to content

Commit ae8ddec

Browse files
author
David Bariod
committed
first basic implementation
0 parents  commit ae8ddec

File tree

7 files changed

+581
-0
lines changed

7 files changed

+581
-0
lines changed

dbfs.go

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
// Package dbfs implement the fs.FS over a sqlite3 database backend.
2+
package dbfs
3+
4+
import (
5+
"database/sql"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"io/fs"
10+
"path"
11+
"strings"
12+
13+
"github.com/hashicorp/go-multierror"
14+
"github.com/jmoiron/sqlx"
15+
_ "github.com/mattn/go-sqlite3"
16+
)
17+
18+
type FS struct {
19+
db *sqlx.DB
20+
rootInode int
21+
}
22+
23+
var (
24+
RelativePathErr = fmt.Errorf("relative path are not supported")
25+
InodeNotFoundErr = fmt.Errorf("cannot find inode")
26+
IncorrectTypeErr = fmt.Errorf("incorrect file type")
27+
)
28+
29+
const (
30+
DirectoryType = "d"
31+
RegularFileType = "f"
32+
)
33+
34+
func NewSqliteFS(dbName string) (*FS, error) {
35+
db, err := sqlx.Open("sqlite3", dbName)
36+
if err != nil {
37+
return nil, fmt.Errorf("canot open the database: %w", err)
38+
}
39+
err = runMigrations(db)
40+
if err != nil {
41+
return nil, err
42+
}
43+
if _, err := db.Exec("PRAGMA foreign_key = ON"); err != nil {
44+
return nil, fmt.Errorf("cannot activate foreign keys check: %w", err)
45+
}
46+
47+
fs := &FS{db: db}
48+
row := db.QueryRow(`
49+
SELECT inode
50+
FROM github_dgsb_dbfs_files
51+
WHERE fname = '/' AND parent IS NULL`)
52+
if err := row.Scan(&fs.rootInode); err != nil {
53+
return nil, fmt.Errorf("no root inode: %w %w", InodeNotFoundErr, err)
54+
}
55+
56+
return fs, nil
57+
}
58+
59+
func (fs *FS) Close() error {
60+
return fs.db.Close()
61+
}
62+
63+
func (fs *FS) addRegularFileNode(tx *sqlx.Tx, fname string) (int, error) {
64+
if !path.IsAbs(fname) {
65+
return 0, fmt.Errorf("%w: %s", RelativePathErr, fname)
66+
}
67+
68+
components := strings.Split(fname, "/")[1:]
69+
var parentInode = fs.rootInode
70+
for i, searchMode := 0, true; i < len(components); i++ {
71+
if searchMode {
72+
var (
73+
inode int
74+
ftype string
75+
)
76+
row := tx.QueryRowx(
77+
"SELECT inode, type FROM github_dgsb_dbfs_files WHERE fname = ? AND parent = ?",
78+
components[i], parentInode)
79+
err := row.Scan(&inode, &ftype)
80+
if err == nil {
81+
parentInode = inode
82+
if (i < len(components)-1 && ftype != DirectoryType) ||
83+
(i == len(components)-1 && ftype != RegularFileType) {
84+
return 0, fmt.Errorf(
85+
"%w: %s %s", IncorrectTypeErr, "/"+strings.Join(components[:i+1], "/"), ftype)
86+
}
87+
continue
88+
}
89+
if !errors.Is(err, sql.ErrNoRows) {
90+
return 0, fmt.Errorf("cannot query files table: %w", err)
91+
}
92+
searchMode = false
93+
}
94+
95+
componentType := func() string {
96+
if i < len(components)-1 {
97+
return DirectoryType
98+
}
99+
return RegularFileType
100+
}()
101+
row := tx.QueryRow(`
102+
INSERT INTO github_dgsb_dbfs_files (fname, parent, type)
103+
VALUES (?, ?, ?)
104+
RETURNING inode`, components[i], parentInode, componentType)
105+
if err := row.Scan(&parentInode); err != nil {
106+
return 0, fmt.Errorf(
107+
"cannot insert node %s as child of %d: %w", components[i], parentInode, err)
108+
}
109+
}
110+
111+
return parentInode, nil
112+
}
113+
114+
func (fs *FS) UpsertFile(fname string, chunkSize int, data []byte) (ret error) {
115+
if !path.IsAbs(fname) {
116+
return fmt.Errorf("%w: %s", RelativePathErr, fname)
117+
}
118+
fname = path.Clean(fname)
119+
120+
tx, err := fs.db.Beginx()
121+
if err != nil {
122+
return fmt.Errorf("cannot start transaction: %w", err)
123+
}
124+
defer func() {
125+
if err != nil {
126+
ret = multierror.Append(ret, tx.Rollback())
127+
} else {
128+
ret = tx.Commit()
129+
}
130+
}()
131+
132+
inode, err := fs.addRegularFileNode(tx, fname)
133+
if err != nil {
134+
return fmt.Errorf("cannot insert file node: %w", err)
135+
}
136+
137+
if _, err := tx.Exec(`DELETE FROM github_dgsb_dbfs_chunks WHERE inode = ?`, inode); err != nil {
138+
return fmt.Errorf("cannot delete previous chunks of the same file %s: %w", fname, err)
139+
}
140+
141+
for i, position := 0, 0; i < len(data); i, position = i+chunkSize, position+1 {
142+
toWrite := func() int {
143+
remaining := len(data) - i
144+
if remaining < chunkSize {
145+
return remaining
146+
}
147+
return chunkSize
148+
}()
149+
_, err := tx.Exec(`
150+
INSERT INTO github_dgsb_dbfs_chunks (inode, position, data, size)
151+
VALUES (?, ?, ?, ?)`, inode, position, data[i:i+toWrite], toWrite)
152+
if err != nil {
153+
return fmt.Errorf("cannot insert file chunk in database: %w", err)
154+
}
155+
}
156+
return nil
157+
}
158+
159+
func (fs *FS) namei(fname string) (int, error) {
160+
if !path.IsAbs(fname) {
161+
return 0, fmt.Errorf("%w: %s", RelativePathErr, fname)
162+
}
163+
components := strings.Split(fname, "/")[1:]
164+
if len(components) == 0 {
165+
return fs.rootInode, nil
166+
}
167+
168+
var inode int
169+
for i, parentInode := 0, fs.rootInode; i < len(components); i, parentInode = i+1, inode {
170+
row := fs.db.QueryRow(
171+
"SELECT inode FROM github_dgsb_dbfs_files WHERE parent = ? AND fname = ?",
172+
parentInode, components[i])
173+
if err := row.Scan(&inode); errors.Is(err, sql.ErrNoRows) {
174+
return 0, fmt.Errorf(
175+
"%w: parent inode %d, fname %s", InodeNotFoundErr, parentInode, components[i])
176+
} else if err != nil {
177+
return 0, fmt.Errorf(
178+
"querying file table: inode %d, fname %s, %w", parentInode, components[i], err)
179+
}
180+
}
181+
return inode, nil
182+
}
183+
184+
func (fs *FS) Open(fname string) (fs.File, error) {
185+
fmt.Println("calling open")
186+
defer fmt.Println("calling open: return")
187+
if !path.IsAbs(fname) {
188+
return nil, fmt.Errorf("relative path are not supported")
189+
}
190+
191+
f := &File{db: fs.db}
192+
inode, err := fs.namei(fname)
193+
if err != nil {
194+
return nil, fmt.Errorf("namei on %s: %w", fname, err)
195+
}
196+
f.inode = inode
197+
198+
row := f.db.QueryRowx(
199+
"SELECT COALESCE(sum(size), 0) FROM github_dgsb_dbfs_chunks WHERE inode = ?", f.inode)
200+
if err := row.Scan(&f.size); err != nil {
201+
return nil, fmt.Errorf("file chunks not found: %d, %w", inode, err)
202+
}
203+
204+
return f, nil
205+
}
206+
207+
type File struct {
208+
db *sqlx.DB
209+
inode int
210+
offset int64
211+
size int64
212+
closed bool
213+
}
214+
215+
func (f *File) Read(out []byte) (int, error) {
216+
fmt.Println("calling read")
217+
defer fmt.Println("calling read: return")
218+
if f.closed {
219+
return 0, fmt.Errorf("file closed")
220+
}
221+
if f.offset >= f.size {
222+
return 0, io.EOF
223+
}
224+
toRead := func(a, b int64) int64 {
225+
if a < b {
226+
return a
227+
}
228+
return b
229+
}(f.size-f.offset, int64(len(out)))
230+
231+
// XXX the query is incorrect
232+
rows, err := f.db.NamedQuery(`
233+
WITH offsets AS (
234+
SELECT
235+
COALESCE(
236+
SUM(size) OVER (
237+
ORDER BY POSITION ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
238+
),
239+
0
240+
) AS start,
241+
position
242+
FROM github_dgsb_dbfs_chunks
243+
WHERE inode = :inode
244+
)
245+
SELECT
246+
github_dgsb_dbfs_chunks.position,
247+
data,
248+
size,
249+
start
250+
FROM github_dgsb_dbfs_chunks JOIN offsets
251+
WHERE inode = :inode
252+
AND :offset < start + size
253+
AND :offset + :size >= start
254+
ORDER BY github_dgsb_dbfs_chunks.position
255+
`, map[string]interface{}{"inode": f.inode, "offset": f.offset, "size": toRead})
256+
if err != nil {
257+
return 0, fmt.Errorf("cannot query the database: %w", err)
258+
}
259+
defer rows.Close()
260+
261+
copied := int64(0)
262+
for rows.Next() {
263+
var (
264+
position int
265+
buf []byte
266+
size int64
267+
offset int64
268+
)
269+
if err := rows.Scan(&position, &buf, &size, &offset); err != nil {
270+
return 0, fmt.Errorf("cannot retrieve file chunk: %w", err)
271+
}
272+
273+
numByte := int64(copy(out[copied:], buf[f.offset-offset:]))
274+
copied += numByte
275+
f.offset += numByte
276+
if copied >= toRead {
277+
break
278+
}
279+
}
280+
if err := rows.Err(); err != nil {
281+
return 0, fmt.Errorf("cannot iterate over file chunks: %w", err)
282+
}
283+
284+
return int(toRead), nil
285+
}
286+
287+
func (f *File) Close() error {
288+
fmt.Println("calling close")
289+
defer fmt.Println("calling close: return")
290+
f.db = nil
291+
f.closed = true
292+
return nil
293+
}
294+
295+
func (f *File) Stat() (fs.FileInfo, error) {
296+
fmt.Println("calling stat")
297+
defer fmt.Println("calling stat: return")
298+
return nil, fmt.Errorf("not yet implemented")
299+
}

dbfs_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package dbfs_test
2+
3+
import (
4+
_ "embed"
5+
"io/fs"
6+
"testing"
7+
8+
. "github.com/dgsb/dbfs"
9+
"github.com/mattn/go-sqlite3"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
//go:embed testdata/le_lac.txt
14+
var leLac string
15+
16+
func TestSqlite3Version(t *testing.T) {
17+
_, num, _ := sqlite3.Version()
18+
require.Equal(t, 3, num/1000_000)
19+
require.Equal(t, 42, (num%1000_000)/1000)
20+
}
21+
22+
func Test_FSCreate(t *testing.T) {
23+
fs, err := NewSqliteFS(":memory:")
24+
require.NoError(t, err)
25+
require.NotNil(t, fs)
26+
t.Cleanup(func() {
27+
require.NoError(t, fs.Close())
28+
})
29+
}
30+
31+
func Test_AddFile(t *testing.T) {
32+
sqlitefs, err := NewSqliteFS("/tmp/test_sqlite3fs.db")
33+
require.NoError(t, err)
34+
require.NotNil(t, sqlitefs)
35+
t.Cleanup(func() {
36+
sqlitefs.Close()
37+
})
38+
39+
err = sqlitefs.UpsertFile("/a/regular/file", 1024, []byte(`bonjour`))
40+
require.NoError(t, err)
41+
42+
f, err := sqlitefs.Open("/a/regular/file")
43+
require.NoError(t, err)
44+
require.NoError(t, f.Close())
45+
46+
check, err := fs.ReadFile(sqlitefs, "/a/regular/file")
47+
require.NoError(t, err)
48+
require.Equal(t, `bonjour`, string(check))
49+
50+
err = sqlitefs.UpsertFile("/poésie/lamartine/le_lac", 32, []byte(leLac))
51+
require.NoError(t, err)
52+
}

go.mod

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module github.com/dgsb/dbfs
2+
3+
go 1.20
4+
5+
require (
6+
github.com/GuiaBolso/darwin v0.0.0-20191218124601-fd6d2aa3d244
7+
github.com/hashicorp/go-multierror v1.1.1
8+
github.com/jmoiron/sqlx v1.3.5
9+
github.com/mattn/go-sqlite3 v1.14.17
10+
)
11+
12+
require (
13+
github.com/DATA-DOG/go-sqlmock v1.5.0 // indirect
14+
github.com/cznic/ql v1.2.0 // indirect
15+
github.com/davecgh/go-spew v1.1.1 // indirect
16+
github.com/hashicorp/errwrap v1.0.0 // indirect
17+
github.com/pmezard/go-difflib v1.0.0 // indirect
18+
github.com/stretchr/testify v1.8.4 // indirect
19+
gopkg.in/yaml.v3 v3.0.1 // indirect
20+
)

0 commit comments

Comments
 (0)