329 lines
7.9 KiB
Go
329 lines
7.9 KiB
Go
package store
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
filterByInclude string = "include"
|
|
filterByExclude string = "exclude"
|
|
|
|
filterByStartsWith string = "starts_with"
|
|
filterByEndsWith string = "ends_with"
|
|
filterByNotStartsWith string = "not_starts_with"
|
|
filterByNotEndsWith string = "not_ends_with"
|
|
|
|
filterByRegex string = "regex"
|
|
filterByNotRegex string = "not_regex"
|
|
|
|
filterByExactMatch string = "exact_match"
|
|
filterByNotExactMatch string = "not_exact_match"
|
|
|
|
filterBySizeGT string = "size_gt"
|
|
filterBySizeLT string = "size_lt"
|
|
|
|
filterBLastAdded string = "last_added"
|
|
)
|
|
|
|
type directoryFilter struct {
|
|
filterType string
|
|
value string
|
|
regex *regexp.Regexp // only for regex/not_regex
|
|
sizeThreshold int64 // only for size_gt/size_lt
|
|
ageThreshold time.Duration // only for last_added
|
|
}
|
|
|
|
type torrents struct {
|
|
sync.RWMutex
|
|
byID map[string]CachedTorrent
|
|
byName map[string]CachedTorrent
|
|
}
|
|
|
|
type folders struct {
|
|
sync.RWMutex
|
|
listing map[string][]os.FileInfo // folder name to file listing
|
|
}
|
|
|
|
type torrentCache struct {
|
|
torrents torrents
|
|
|
|
listing atomic.Value
|
|
folders folders
|
|
directoriesFilters map[string][]directoryFilter
|
|
sortNeeded atomic.Bool
|
|
}
|
|
|
|
type sortableFile struct {
|
|
id string
|
|
name string
|
|
modTime time.Time
|
|
size int64
|
|
bad bool
|
|
}
|
|
|
|
func newTorrentCache(dirFilters map[string][]directoryFilter) *torrentCache {
|
|
|
|
tc := &torrentCache{
|
|
torrents: torrents{
|
|
byID: make(map[string]CachedTorrent),
|
|
byName: make(map[string]CachedTorrent),
|
|
},
|
|
folders: folders{
|
|
listing: make(map[string][]os.FileInfo),
|
|
},
|
|
directoriesFilters: dirFilters,
|
|
}
|
|
|
|
tc.sortNeeded.Store(false)
|
|
tc.listing.Store(make([]os.FileInfo, 0))
|
|
return tc
|
|
}
|
|
|
|
func (tc *torrentCache) reset() {
|
|
tc.torrents.Lock()
|
|
tc.torrents.byID = make(map[string]CachedTorrent)
|
|
tc.torrents.byName = make(map[string]CachedTorrent)
|
|
tc.torrents.Unlock()
|
|
|
|
// reset the sorted listing
|
|
tc.sortNeeded.Store(false)
|
|
tc.listing.Store(make([]os.FileInfo, 0))
|
|
|
|
// reset any per-folder views
|
|
tc.folders.Lock()
|
|
tc.folders.listing = make(map[string][]os.FileInfo)
|
|
tc.folders.Unlock()
|
|
}
|
|
|
|
func (tc *torrentCache) getByID(id string) (CachedTorrent, bool) {
|
|
tc.torrents.RLock()
|
|
defer tc.torrents.RUnlock()
|
|
torrent, exists := tc.torrents.byID[id]
|
|
return torrent, exists
|
|
}
|
|
|
|
func (tc *torrentCache) getByName(name string) (CachedTorrent, bool) {
|
|
tc.torrents.RLock()
|
|
defer tc.torrents.RUnlock()
|
|
torrent, exists := tc.torrents.byName[name]
|
|
return torrent, exists
|
|
}
|
|
|
|
func (tc *torrentCache) set(name string, torrent, newTorrent CachedTorrent) {
|
|
tc.torrents.Lock()
|
|
// Set the id first
|
|
|
|
tc.torrents.byName[name] = torrent
|
|
tc.torrents.byID[torrent.Id] = torrent // This is the unadulterated torrent
|
|
tc.torrents.Unlock()
|
|
tc.sortNeeded.Store(true)
|
|
}
|
|
|
|
func (tc *torrentCache) getListing() []os.FileInfo {
|
|
// Fast path: if we have a sorted list and no changes since last sort
|
|
if !tc.sortNeeded.Load() {
|
|
return tc.listing.Load().([]os.FileInfo)
|
|
}
|
|
|
|
// Slow path: need to sort
|
|
tc.refreshListing()
|
|
return tc.listing.Load().([]os.FileInfo)
|
|
}
|
|
|
|
func (tc *torrentCache) getFolderListing(folderName string) []os.FileInfo {
|
|
tc.folders.RLock()
|
|
defer tc.folders.RUnlock()
|
|
if folderName == "" {
|
|
return tc.getListing()
|
|
}
|
|
if folder, ok := tc.folders.listing[folderName]; ok {
|
|
return folder
|
|
}
|
|
// If folder not found, return empty slice
|
|
return []os.FileInfo{}
|
|
}
|
|
|
|
func (tc *torrentCache) refreshListing() {
|
|
|
|
tc.torrents.RLock()
|
|
all := make([]sortableFile, 0, len(tc.torrents.byName))
|
|
for name, t := range tc.torrents.byName {
|
|
all = append(all, sortableFile{t.Id, name, t.AddedOn, t.Bytes, t.Bad})
|
|
}
|
|
tc.sortNeeded.Store(false)
|
|
tc.torrents.RUnlock()
|
|
|
|
sort.Slice(all, func(i, j int) bool {
|
|
if all[i].name != all[j].name {
|
|
return all[i].name < all[j].name
|
|
}
|
|
return all[i].modTime.Before(all[j].modTime)
|
|
})
|
|
|
|
wg := sync.WaitGroup{}
|
|
|
|
wg.Add(1) // for all listing
|
|
go func() {
|
|
defer wg.Done()
|
|
listing := make([]os.FileInfo, len(all))
|
|
for i, sf := range all {
|
|
listing[i] = &fileInfo{sf.id, sf.name, sf.size, 0755 | os.ModeDir, sf.modTime, true}
|
|
}
|
|
tc.listing.Store(listing)
|
|
}()
|
|
|
|
wg.Add(1)
|
|
// For __bad__
|
|
go func() {
|
|
defer wg.Done()
|
|
listing := make([]os.FileInfo, 0)
|
|
for _, sf := range all {
|
|
if sf.bad {
|
|
listing = append(listing, &fileInfo{
|
|
id: sf.id,
|
|
name: fmt.Sprintf("%s || %s", sf.name, sf.id),
|
|
size: sf.size,
|
|
mode: 0755 | os.ModeDir,
|
|
modTime: sf.modTime,
|
|
isDir: true,
|
|
})
|
|
}
|
|
}
|
|
tc.folders.Lock()
|
|
if len(listing) > 0 {
|
|
tc.folders.listing["__bad__"] = listing
|
|
} else {
|
|
delete(tc.folders.listing, "__bad__")
|
|
}
|
|
tc.folders.Unlock()
|
|
}()
|
|
|
|
now := time.Now()
|
|
wg.Add(len(tc.directoriesFilters)) // for each directory filter
|
|
for dir, filters := range tc.directoriesFilters {
|
|
go func(dir string, filters []directoryFilter) {
|
|
defer wg.Done()
|
|
var matched []os.FileInfo
|
|
for _, sf := range all {
|
|
if tc.torrentMatchDirectory(filters, sf, now) {
|
|
matched = append(matched, &fileInfo{
|
|
id: sf.id,
|
|
name: sf.name, size: sf.size,
|
|
mode: 0755 | os.ModeDir, modTime: sf.modTime, isDir: true,
|
|
})
|
|
}
|
|
}
|
|
|
|
tc.folders.Lock()
|
|
if len(matched) > 0 {
|
|
tc.folders.listing[dir] = matched
|
|
} else {
|
|
delete(tc.folders.listing, dir)
|
|
}
|
|
tc.folders.Unlock()
|
|
}(dir, filters)
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func (tc *torrentCache) torrentMatchDirectory(filters []directoryFilter, file sortableFile, now time.Time) bool {
|
|
|
|
torrentName := strings.ToLower(file.name)
|
|
for _, filter := range filters {
|
|
matched := false
|
|
|
|
switch filter.filterType {
|
|
case filterByInclude:
|
|
matched = strings.Contains(torrentName, filter.value)
|
|
case filterByStartsWith:
|
|
matched = strings.HasPrefix(torrentName, filter.value)
|
|
case filterByEndsWith:
|
|
matched = strings.HasSuffix(torrentName, filter.value)
|
|
case filterByExactMatch:
|
|
matched = torrentName == filter.value
|
|
case filterByExclude:
|
|
matched = !strings.Contains(torrentName, filter.value)
|
|
case filterByNotStartsWith:
|
|
matched = !strings.HasPrefix(torrentName, filter.value)
|
|
case filterByNotEndsWith:
|
|
matched = !strings.HasSuffix(torrentName, filter.value)
|
|
case filterByRegex:
|
|
matched = filter.regex.MatchString(torrentName)
|
|
case filterByNotRegex:
|
|
matched = !filter.regex.MatchString(torrentName)
|
|
case filterByNotExactMatch:
|
|
matched = torrentName != filter.value
|
|
case filterBySizeGT:
|
|
matched = file.size > filter.sizeThreshold
|
|
case filterBySizeLT:
|
|
matched = file.size < filter.sizeThreshold
|
|
case filterBLastAdded:
|
|
matched = file.modTime.After(now.Add(-filter.ageThreshold))
|
|
}
|
|
if !matched {
|
|
return false // All filters must match
|
|
}
|
|
}
|
|
|
|
// If we get here, all filters matched
|
|
return true
|
|
}
|
|
|
|
func (tc *torrentCache) getAll() map[string]CachedTorrent {
|
|
tc.torrents.RLock()
|
|
defer tc.torrents.RUnlock()
|
|
result := make(map[string]CachedTorrent, len(tc.torrents.byID))
|
|
for name, torrent := range tc.torrents.byID {
|
|
result[name] = torrent
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (tc *torrentCache) getAllCount() int {
|
|
tc.torrents.RLock()
|
|
defer tc.torrents.RUnlock()
|
|
return len(tc.torrents.byID)
|
|
}
|
|
|
|
func (tc *torrentCache) getAllByName() map[string]CachedTorrent {
|
|
tc.torrents.RLock()
|
|
defer tc.torrents.RUnlock()
|
|
results := make(map[string]CachedTorrent, len(tc.torrents.byName))
|
|
for name, torrent := range tc.torrents.byName {
|
|
results[name] = torrent
|
|
}
|
|
return results
|
|
}
|
|
|
|
func (tc *torrentCache) getIdMaps() map[string]struct{} {
|
|
tc.torrents.RLock()
|
|
defer tc.torrents.RUnlock()
|
|
res := make(map[string]struct{}, len(tc.torrents.byID))
|
|
for id := range tc.torrents.byID {
|
|
res[id] = struct{}{}
|
|
}
|
|
return res
|
|
}
|
|
|
|
func (tc *torrentCache) removeId(id string) {
|
|
tc.torrents.Lock()
|
|
defer tc.torrents.Unlock()
|
|
delete(tc.torrents.byID, id)
|
|
tc.sortNeeded.Store(true)
|
|
}
|
|
|
|
func (tc *torrentCache) remove(name string) {
|
|
tc.torrents.Lock()
|
|
defer tc.torrents.Unlock()
|
|
delete(tc.torrents.byName, name)
|
|
tc.sortNeeded.Store(true)
|
|
}
|