Files
2026-07-27 19:31:43 +08:00

329 lines
8.2 KiB
Go

package fusereadcache
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"time"
_ "modernc.org/sqlite"
)
type blockMeta struct {
AccountID int64
FileID string
BlockIdx int64
ByteLen int64
CreatedAt int64
AccessedAt int64
}
type blockKey struct {
AccountID int64
FileID string
BlockIdx int64
}
type storeLayer struct {
root string
blocks string
db *sql.DB
lastTouches map[blockKey]int64
}
func openStore(ctx context.Context, root string) (*storeLayer, error) {
if err := os.MkdirAll(filepath.Join(root, "blocks"), 0o755); err != nil {
return nil, err
}
dsn := "file:" + filepath.Join(root, "index.sqlite") +
"?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)" +
"&_pragma=foreign_keys(on)&_pragma=synchronous(NORMAL)"
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(1)
if err := db.PingContext(ctx); err != nil {
_ = db.Close()
return nil, err
}
if _, err := db.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS blocks (
account_id INTEGER NOT NULL,
file_id TEXT NOT NULL,
block_idx INTEGER NOT NULL,
byte_len INTEGER NOT NULL,
created_at INTEGER NOT NULL,
accessed_at INTEGER NOT NULL,
PRIMARY KEY (account_id, file_id, block_idx)
);
CREATE INDEX IF NOT EXISTS idx_blocks_accessed ON blocks(accessed_at);
`); err != nil {
_ = db.Close()
return nil, err
}
return &storeLayer{
root: root,
blocks: filepath.Join(root, "blocks"),
db: db,
lastTouches: make(map[blockKey]int64),
}, nil
}
func (s *storeLayer) Close() error {
if s == nil || s.db == nil {
return nil
}
return s.db.Close()
}
func fileDir(fileID string) string {
sum := sha256.Sum256([]byte(fileID))
return hex.EncodeToString(sum[:16])
}
func (s *storeLayer) blockPath(accountID int64, fileID string, blockIdx int64) string {
return filepath.Join(s.blocks, strconv.FormatInt(accountID, 10), fileDir(fileID), strconv.FormatInt(blockIdx, 10)+".bin")
}
func (s *storeLayer) loadBlockRange(accountID int64, fileID string, blockIdx, blockOff int64, dest []byte) (int, bool, error) {
path := s.blockPath(accountID, fileID, blockIdx)
f, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return 0, false, nil
}
return 0, false, err
}
defer f.Close()
n, readErr := f.ReadAt(dest, blockOff)
if readErr != nil && readErr != io.EOF {
return n, true, readErr
}
if err := s.touchBlock(accountID, fileID, blockIdx); err != nil {
return n, true, err
}
return n, true, readErr
}
func (s *storeLayer) touchBlock(accountID int64, fileID string, blockIdx int64) error {
now := time.Now().Unix()
key := blockKey{AccountID: accountID, FileID: fileID, BlockIdx: blockIdx}
if s.lastTouches[key] == now {
return nil
}
_, err := s.db.Exec(`
UPDATE blocks SET accessed_at=? WHERE account_id=? AND file_id=? AND block_idx=?`,
now, accountID, fileID, blockIdx)
if err == nil {
s.lastTouches[key] = now
}
return err
}
func (s *storeLayer) putBlock(accountID int64, fileID string, blockIdx int64, data []byte) error {
if len(data) == 0 {
return nil
}
path := s.blockPath(accountID, fileID, blockIdx)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
tmp := path + ".tmp"
if err := os.WriteFile(tmp, data, 0o644); err != nil {
return err
}
if err := os.Rename(tmp, path); err != nil {
_ = os.Remove(tmp)
return err
}
now := time.Now().Unix()
_, err := s.db.Exec(`
INSERT INTO blocks(account_id,file_id,block_idx,byte_len,created_at,accessed_at)
VALUES(?,?,?,?,?,?)
ON CONFLICT(account_id,file_id,block_idx) DO UPDATE SET
byte_len=excluded.byte_len,
accessed_at=excluded.accessed_at`,
accountID, fileID, blockIdx, len(data), now, now)
if err == nil {
s.lastTouches[blockKey{AccountID: accountID, FileID: fileID, BlockIdx: blockIdx}] = now
}
return err
}
func (s *storeLayer) deleteBlock(meta blockMeta) error {
_ = os.Remove(s.blockPath(meta.AccountID, meta.FileID, meta.BlockIdx))
delete(s.lastTouches, blockKey{AccountID: meta.AccountID, FileID: meta.FileID, BlockIdx: meta.BlockIdx})
_, err := s.db.Exec(`DELETE FROM blocks WHERE account_id=? AND file_id=? AND block_idx=?`,
meta.AccountID, meta.FileID, meta.BlockIdx)
return err
}
func (s *storeLayer) stats() (used int64, blocks int64, err error) {
row := s.db.QueryRow(`SELECT COALESCE(SUM(byte_len),0), COUNT(*) FROM blocks`)
if err := row.Scan(&used, &blocks); err != nil {
return 0, 0, err
}
return used, blocks, nil
}
func (s *storeLayer) expireBefore(cutoff int64) error {
rows, err := s.db.Query(`
SELECT account_id,file_id,block_idx,byte_len,created_at,accessed_at
FROM blocks WHERE created_at < ?`, cutoff)
if err != nil {
return err
}
var metas []blockMeta
for rows.Next() {
var m blockMeta
if err := rows.Scan(&m.AccountID, &m.FileID, &m.BlockIdx, &m.ByteLen, &m.CreatedAt, &m.AccessedAt); err != nil {
_ = rows.Close()
return err
}
metas = append(metas, m)
}
if err := rows.Close(); err != nil {
return err
}
if err := rows.Err(); err != nil {
return err
}
for _, m := range metas {
if err := s.deleteBlock(m); err != nil {
return err
}
}
return nil
}
func (s *storeLayer) pickEvictLRU() (blockMeta, bool, error) {
row := s.db.QueryRow(`
SELECT account_id,file_id,block_idx,byte_len,created_at,accessed_at
FROM blocks ORDER BY accessed_at ASC LIMIT 1`)
var m blockMeta
if err := row.Scan(&m.AccountID, &m.FileID, &m.BlockIdx, &m.ByteLen, &m.CreatedAt, &m.AccessedAt); err != nil {
if err == sql.ErrNoRows {
return blockMeta{}, false, nil
}
return blockMeta{}, false, err
}
return m, true, nil
}
func (s *storeLayer) pickEvictLargeFile() (blockMeta, bool, error) {
row := s.db.QueryRow(`
SELECT b.account_id,b.file_id,b.block_idx,b.byte_len,b.created_at,b.accessed_at
FROM blocks b
JOIN (
SELECT account_id,file_id,SUM(byte_len) AS total
FROM blocks GROUP BY account_id,file_id
ORDER BY total DESC LIMIT 1
) t ON b.account_id=t.account_id AND b.file_id=t.file_id
ORDER BY b.accessed_at ASC LIMIT 1`)
var m blockMeta
if err := row.Scan(&m.AccountID, &m.FileID, &m.BlockIdx, &m.ByteLen, &m.CreatedAt, &m.AccessedAt); err != nil {
if err == sql.ErrNoRows {
return blockMeta{}, false, nil
}
return blockMeta{}, false, err
}
return m, true, nil
}
func (s *storeLayer) invalidateFile(accountID int64, fileID string) error {
rows, err := s.db.Query(`
SELECT account_id,file_id,block_idx,byte_len,created_at,accessed_at
FROM blocks WHERE account_id=? AND file_id=?`, accountID, fileID)
if err != nil {
return err
}
var metas []blockMeta
for rows.Next() {
var m blockMeta
if err := rows.Scan(&m.AccountID, &m.FileID, &m.BlockIdx, &m.ByteLen, &m.CreatedAt, &m.AccessedAt); err != nil {
_ = rows.Close()
return err
}
metas = append(metas, m)
}
if err := rows.Close(); err != nil {
return err
}
if err := rows.Err(); err != nil {
return err
}
for _, m := range metas {
if err := s.deleteBlock(m); err != nil {
return err
}
}
_ = os.RemoveAll(filepath.Join(s.blocks, strconv.FormatInt(accountID, 10), fileDir(fileID)))
return nil
}
func (s *storeLayer) invalidateAccount(accountID int64) error {
rows, err := s.db.Query(`
SELECT account_id,file_id,block_idx,byte_len,created_at,accessed_at
FROM blocks WHERE account_id=?`, accountID)
if err != nil {
return err
}
var metas []blockMeta
for rows.Next() {
var m blockMeta
if err := rows.Scan(&m.AccountID, &m.FileID, &m.BlockIdx, &m.ByteLen, &m.CreatedAt, &m.AccessedAt); err != nil {
_ = rows.Close()
return err
}
metas = append(metas, m)
}
if err := rows.Close(); err != nil {
return err
}
if err := rows.Err(); err != nil {
return err
}
for _, m := range metas {
if err := s.deleteBlock(m); err != nil {
return err
}
}
_ = os.RemoveAll(filepath.Join(s.blocks, strconv.FormatInt(accountID, 10)))
return nil
}
func (s *storeLayer) clearAll() error {
if err := os.RemoveAll(s.blocks); err != nil {
return err
}
if err := os.MkdirAll(s.blocks, 0o755); err != nil {
return err
}
_, err := s.db.Exec(`DELETE FROM blocks`)
if err == nil {
clear(s.lastTouches)
}
return err
}
func (s *storeLayer) rootPath() string {
if s == nil {
return ""
}
return s.root
}
func validateRoot(root string) error {
if root == "" {
return fmt.Errorf("empty cache root")
}
return nil
}