aboutsummaryrefslogtreecommitdiff
path: root/src/gnunet/service/store/store_dht_meta.go
blob: d1c4cb7f27b165c9d248c049f622e859853ffc30 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
// This file is part of gnunet-go, a GNUnet-implementation in Golang.
// Copyright (C) 2019-2022 Bernd Fix  >Y<
//
// gnunet-go is free software: you can redistribute it and/or modify it
// under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License,
// or (at your option) any later version.
//
// gnunet-go is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: AGPL3.0-or-later

package store

import (
	"database/sql"
	_ "embed"
	"gnunet/crypto"
	"gnunet/enums"
	"gnunet/service/dht/blocks"
	"gnunet/util"
	"os"
)

//============================================================
// Metadata handling for file storage
//============================================================

// FileMetadata holds information about a file (raw block data)
// and is stored in a SQL database for faster access.
type FileMetadata struct {
	key       *crypto.HashCode  // storage key
	size      uint64            // size of file
	btype     enums.BlockType   // block type
	bhash     *crypto.HashCode  // block hash
	stored    util.AbsoluteTime // time added to store
	expires   util.AbsoluteTime // expiration time
	lastUsed  util.AbsoluteTime // time last used
	usedCount uint64            // usage count
}

// NewFileMetadata creates a new file metadata instance
func NewFileMetadata() *FileMetadata {
	return &FileMetadata{
		key:   crypto.NewHashCode(nil),
		bhash: crypto.NewHashCode(nil),
	}
}

//------------------------------------------------------------
// Metadata database: A SQLite3 database to hold metadata about
// blocks in file storage
//------------------------------------------------------------

//go:embed store_dht_meta.sql
var initScript []byte

// FileMetaDB is a SQLite3 database for block metadata
type FileMetaDB struct {
	conn *DBConn // database connection
}

// OpenMetaDB opens a metadata database in the given path. The name of the
// database is "access.db".
func OpenMetaDB(path string) (db *FileMetaDB, err error) {
	// connect to database
	dbFile := path + "/access.db"
	if _, err = os.Stat(path + "/access.db"); err != nil {
		var file *os.File
		if file, err = os.Create(dbFile); err != nil {
			return
		}
		file.Close()
	}
	db = new(FileMetaDB)
	if db.conn, err = DBPool.Connect("sqlite3:" + dbFile); err != nil {
		return
	}
	// check for initialized database
	res := db.conn.QueryRow("select name from sqlite_master where type='table' and name='meta'")
	var s string
	if res.Scan(&s) != nil {
		// initialize database
		if _, err = db.conn.Exec(string(initScript)); err != nil {
			return
		}
	}
	return
}

// Store metadata in database: creates or updates a record for the metadata
// in the database; primary key is the query key
func (db *FileMetaDB) Store(md *FileMetadata) (err error) {
	// work around a SQLite3 bug when storing uint64 with high bit set
	var exp *uint64
	if !md.expires.IsNever() {
		exp = new(uint64)
		*exp = md.expires.Val
	}
	sql := "replace into meta(qkey,btype,bhash,size,stored,expires,lastUsed,usedCount) values(?,?,?,?,?,?,?,?)"
	_, err = db.conn.Exec(sql,
		md.key.Data, md.btype, md.bhash.Data, md.size, md.stored.Epoch(),
		exp, md.lastUsed.Epoch(), md.usedCount)
	return
}

// Get block metadata from database
func (db *FileMetaDB) Get(query blocks.Query) (mds []*FileMetadata, err error) {
	// select rows in database matching the query
	stmt := "select size,bhash,stored,expires,lastUsed,usedCount from meta where qkey=?"
	btype := query.Type()
	var rows *sql.Rows
	if btype == enums.BLOCK_TYPE_ANY {
		rows, err = db.conn.Query(stmt, query.Key().Data)
	} else {
		rows, err = db.conn.Query(stmt+" and btype=?", query.Key().Data, btype)
	}
	if err != nil {
		return
	}
	// process results
	for rows.Next() {
		md := NewFileMetadata()
		md.key = query.Key()
		md.btype = btype
		var st, lu uint64
		var exp *uint64
		if err = rows.Scan(&md.size, &md.bhash.Data, &st, &exp, &lu, &md.usedCount); err != nil {
			if err == sql.ErrNoRows {
				md = nil
				err = nil
			}
			return
		}
		if exp != nil {
			md.expires.Val = *exp
		} else {
			md.expires = util.AbsoluteTimeNever()
		}
		md.stored.Val = st * 1000000
		md.lastUsed.Val = lu * 1000000
		mds = append(mds, md)
	}
	return
}

// Drop metadata for block from database
func (db *FileMetaDB) Drop(key []byte, btype enums.BlockType) (err error) {
	if btype != enums.BLOCK_TYPE_ANY {
		_, err = db.conn.Exec("delete from meta where qkey=? and btype=?", key, btype)
	} else {
		_, err = db.conn.Exec("delete from meta where qkey=?", key)
	}
	return
}

// Used a block from store: increment usage count and lastUsed time.
func (db *FileMetaDB) Used(key []byte, btype enums.BlockType) (err error) {
	stmt := "update meta set usedCount=usedCount+1,lastUsed=unixepoch() where qkey=?"
	if btype != enums.BLOCK_TYPE_ANY {
		_, err = db.conn.Exec(stmt+" and btype=?", key, btype)
	} else {
		_, err = db.conn.Exec(stmt, key)
	}
	return
}

// Obsolete collects records from the meta database that are considered
// "removable". Entries are rated by the value of "(lifetime * size) / usedCount"
func (db *FileMetaDB) Obsolete(n int) (removable []*FileMetadata, err error) {
	// get obsolete records from database
	rate := "(unixepoch()-unixepoch(stored))*size/usedCount"
	stmt := "select qkey,btype from meta order by " + rate + " limit ?"
	var rows *sql.Rows
	if rows, err = db.conn.Query(stmt, n); err != nil {
		return
	}
	var md *FileMetadata
	for rows.Next() {
		var st, lu uint64
		if err = rows.Scan(&md.key, &md.btype, &md.size, &st, &md.expires.Val, &lu, &md.usedCount); err != nil {
			return
		}
		md.stored.Val = st * 1000000
		md.lastUsed.Val = lu * 1000000
		removable = append(removable, md)
	}
	return
}

// Traverse metadata records and call function on each record
func (db *FileMetaDB) Traverse(f func(*FileMetadata)) error {
	sql := "select qkey,btype,bhash,size,stored,expires,lastUsed,usedCount from meta"
	rows, err := db.conn.Query(sql)
	if err != nil {
		return err
	}
	md := NewFileMetadata()
	for rows.Next() {
		var st, lu uint64
		var exp *uint64
		err = rows.Scan(&md.key.Data, &md.btype, &md.bhash.Data, &md.size, &st, &exp, &lu, &md.usedCount)
		if err != nil {
			return err
		}
		if exp != nil {
			md.expires.Val = *exp
		} else {
			md.expires = util.AbsoluteTimeNever()
		}
		md.stored.Val = st * 1000000
		md.lastUsed.Val = lu * 1000000
		// call process function
		f(md)
	}
	return nil
}

// Close metadata database
func (db *FileMetaDB) Close() error {
	return db.conn.Close()
}