mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-10-28 04:05:21 +08:00
267 lines
6.9 KiB
Go
267 lines
6.9 KiB
Go
package indexer
|
|
|
|
import (
|
|
"database/sql"
|
|
"io"
|
|
"io/fs"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
. "github.com/mickael-kerjean/filestash/server/common"
|
|
)
|
|
|
|
type Index interface {
|
|
Init() error
|
|
Search(path string, q string) ([]IFile, error)
|
|
Change() (Manager, error)
|
|
Close() error
|
|
}
|
|
|
|
type Manager interface {
|
|
FindParent(path string) (RowMapper, error)
|
|
FindBefore(time time.Time) (RowMapper, error)
|
|
FindNew(maxSize int, toOmit []string) (RowMapper, error)
|
|
|
|
FileCreate(f fs.FileInfo, parent string) error
|
|
FileContentUpdate(path string, f io.ReadCloser) error
|
|
FileMetaUpdate(path string, f fs.FileInfo) error
|
|
|
|
IndexTimeGet(path string) (time.Time, error)
|
|
IndexTimeUpdate(path string, t time.Time) error
|
|
IndexTimeClear(path string) error
|
|
|
|
RemoveAll(path string) error
|
|
Commit() error
|
|
}
|
|
|
|
func NewIndex(id string) Index {
|
|
return &sqliteIndex{id, nil}
|
|
}
|
|
|
|
type sqliteIndex struct {
|
|
id string
|
|
db *sql.DB
|
|
}
|
|
|
|
func (this *sqliteIndex) Init() error {
|
|
path := GetAbsolutePath(FTS_PATH, "fts_"+this.id+".sql")
|
|
db, err := sql.Open("sqlite3", path+"?_journal_mode=wal")
|
|
if err != nil {
|
|
Log.Warning("search::init can't open database (%v)", err)
|
|
return toErr(err)
|
|
}
|
|
this.db = db
|
|
|
|
queryDB := func(sqlQuery string) error {
|
|
stmt, err := db.Prepare(sqlQuery)
|
|
if err != nil {
|
|
Log.Warning("search::initschema prepare schema error(%v)", err)
|
|
return toErr(err)
|
|
}
|
|
defer stmt.Close()
|
|
_, err = stmt.Exec()
|
|
if err != nil {
|
|
Log.Warning("search::initschema execute error(%v)", err)
|
|
return toErr(err)
|
|
}
|
|
return nil
|
|
}
|
|
if queryDB("CREATE TABLE IF NOT EXISTS file(path VARCHAR(1024) PRIMARY KEY, filename VARCHAR(64), filetype VARCHAR(16), type VARCHAR(16), parent VARCHAR(1024), size INTEGER, modTime timestamp, indexTime timestamp DEFAULT NULL);"); err != nil {
|
|
return err
|
|
}
|
|
if queryDB("CREATE INDEX IF NOT EXISTS idx_file_index_time ON file(indexTime) WHERE indexTime IS NOT NULL;"); err != nil {
|
|
return err
|
|
}
|
|
if queryDB("CREATE INDEX IF NOT EXISTS idx_file_parent ON file(parent);"); err != nil {
|
|
return err
|
|
}
|
|
if queryDB("CREATE VIRTUAL TABLE IF NOT EXISTS file_index USING fts5(path UNINDEXED, filename, filetype, content, tokenize = 'porter');"); err != nil {
|
|
return err
|
|
}
|
|
if queryDB("CREATE TRIGGER IF NOT EXISTS after_file_insert AFTER INSERT ON file BEGIN INSERT INTO file_index (path, filename, filetype) VALUES(new.path, new.filename, new.filetype); END;"); err != nil {
|
|
return err
|
|
}
|
|
if queryDB("CREATE TRIGGER IF NOT EXISTS after_file_delete AFTER DELETE ON file BEGIN DELETE FROM file_index WHERE path = old.path; END;"); err != nil {
|
|
return err
|
|
}
|
|
if queryDB("CREATE TRIGGER IF NOT EXISTS after_file_update_path UPDATE OF path ON file BEGIN UPDATE file_index SET path = new.path, filepath = new.filepath, filetype = new.filetype WHERE path = old.path; END;"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (this sqliteIndex) Change() (Manager, error) {
|
|
tx, err := this.db.Begin()
|
|
if err != nil {
|
|
return nil, toErr(err)
|
|
}
|
|
return sqliteQueries{tx}, nil
|
|
}
|
|
|
|
func (this sqliteIndex) Close() error {
|
|
return this.db.Close()
|
|
}
|
|
|
|
type sqliteQueries struct {
|
|
tx *sql.Tx
|
|
}
|
|
|
|
func (this sqliteQueries) Commit() error {
|
|
return this.tx.Commit()
|
|
}
|
|
|
|
func (this sqliteQueries) IndexTimeGet(path string) (time.Time, error) {
|
|
var t string
|
|
if err := this.tx.QueryRow("SELECT indexTime FROM file WHERE path = ?", path).Scan(&t); err != nil {
|
|
return time.Now(), toErr(err)
|
|
}
|
|
tm, err := time.Parse(time.RFC3339, t)
|
|
if err != nil {
|
|
return tm, toErr(err)
|
|
}
|
|
return tm, nil
|
|
}
|
|
|
|
func (this sqliteQueries) IndexTimeUpdate(path string, time time.Time) error {
|
|
if _, err := this.tx.Exec("UPDATE file SET indexTime = ? WHERE path = ?", time, path); err != nil {
|
|
return toErr(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (this sqliteQueries) IndexTimeClear(path string) error {
|
|
if _, err := this.tx.Exec("UPDATE file SET indexTime = NULL WHERE path = ?", path); err != nil {
|
|
return toErr(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Record struct {
|
|
Name string
|
|
Path string
|
|
Size int64
|
|
CType string
|
|
}
|
|
|
|
type RowMapper struct {
|
|
rows *sql.Rows
|
|
}
|
|
|
|
func (this *RowMapper) Next() bool {
|
|
return this.rows.Next()
|
|
}
|
|
|
|
func (this *RowMapper) Value() (Record, error) {
|
|
var r Record
|
|
if err := this.rows.Scan(&r.Name, &r.CType, &r.Path, &r.Size); err != nil {
|
|
return r, toErr(err)
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func (this *RowMapper) Close() error {
|
|
return this.rows.Close()
|
|
}
|
|
|
|
func (this sqliteQueries) FindNew(maxSize int, toOmit []string) (RowMapper, error) {
|
|
for i := 0; i < len(toOmit); i++ {
|
|
toOmit[i] = "'" + strings.TrimSpace(toOmit[i]) + "'"
|
|
}
|
|
|
|
rows, err := this.tx.Query(
|
|
"SELECT filename, type, path, size FROM file WHERE ("+
|
|
" type = 'file' AND size < ? AND filetype IN ("+strings.Join(toOmit, ",")+") AND indexTime IS NULL "+
|
|
") LIMIT 2",
|
|
maxSize,
|
|
)
|
|
if err != nil {
|
|
return RowMapper{}, toErr(err)
|
|
}
|
|
return RowMapper{rows: rows}, nil
|
|
}
|
|
|
|
func (this sqliteQueries) FindBefore(t time.Time) (RowMapper, error) {
|
|
rows, err := this.tx.Query(
|
|
"SELECT filename, type, path, size FROM file WHERE indexTime < ? ORDER BY indexTime DESC LIMIT 5",
|
|
t,
|
|
)
|
|
if err != nil {
|
|
return RowMapper{}, toErr(err)
|
|
}
|
|
return RowMapper{rows: rows}, nil
|
|
}
|
|
|
|
func (this sqliteQueries) FindParent(path string) (RowMapper, error) {
|
|
rows, err := this.tx.Query("SELECT filename, type, path, size FROM file WHERE parent = ?", path)
|
|
if err != nil {
|
|
return RowMapper{}, err
|
|
}
|
|
return RowMapper{rows: rows}, nil
|
|
}
|
|
|
|
func (this sqliteQueries) FileMetaUpdate(path string, f fs.FileInfo) error {
|
|
_, err := this.tx.Exec(
|
|
"UPDATE file SET size = ?, modTime = ? indexTime = NULL WHERE path = ?",
|
|
f.Size(), f.ModTime(), path,
|
|
)
|
|
return toErr(err)
|
|
}
|
|
|
|
func (this sqliteQueries) FileContentUpdate(path string, reader io.ReadCloser) error {
|
|
content, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
return toErr(err)
|
|
}
|
|
if _, err := this.tx.Exec("UPDATE file_index SET content = ? WHERE path = ?", content, path); err != nil {
|
|
return toErr(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (this sqliteQueries) FileCreate(f fs.FileInfo, parentPath string) (err error) {
|
|
name := f.Name()
|
|
path := filepath.Join(parentPath, f.Name())
|
|
if f.IsDir() {
|
|
_, err = this.tx.Exec(
|
|
"INSERT INTO file(path, parent, filename, type, size, modTime, indexTime) "+
|
|
"VALUES(?, ?, ?, ?, ?, ?, ?)",
|
|
path+"/",
|
|
parentPath,
|
|
name,
|
|
"directory",
|
|
f.Size(),
|
|
f.ModTime(),
|
|
time.Now(),
|
|
)
|
|
} else {
|
|
_, err = this.tx.Exec(
|
|
"INSERT INTO file(path, parent, filename, type, size, modTime, indexTime, filetype) "+
|
|
"VALUES(?, ?, ?, ?, ?, ?, ?, ?)",
|
|
path,
|
|
parentPath,
|
|
name,
|
|
"file",
|
|
f.Size(),
|
|
f.ModTime(),
|
|
nil,
|
|
strings.TrimPrefix(filepath.Ext(name), "."),
|
|
)
|
|
}
|
|
return toErr(err)
|
|
}
|
|
|
|
func (this sqliteQueries) Remove(path string) error {
|
|
if _, a := this.tx.Exec("DELETE FROM file WHERE path = ?", path); a != nil {
|
|
return toErr(a)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (this sqliteQueries) RemoveAll(path string) error {
|
|
if _, a := this.tx.Exec("DELETE FROM file WHERE path >= ? AND path < ?", path, path+"~"); a != nil {
|
|
return toErr(a)
|
|
}
|
|
return nil
|
|
}
|