package main
import (
"bytes"
"html/template"
"io/ioutil"
"log"
"net"
"os"
"strings"
"time"
"github.com/getwtxt/registry"
"github.com/syndtr/goleveldb/leveldb"
)
// Checks whether it's time to refresh
// the cache.
func checkCacheTime() bool {
return time.Since(confObj.LastCache) > confObj.CacheInterval
}
// Checks whether it's time to push
// the cache to the database
func checkDBtime() bool {
return time.Since(confObj.LastPush) > confObj.DBInterval
}
// Launched by init as a goroutine to constantly watch
// for the update interval to pass.
func cacheAndPush() {
for {
if checkCacheTime() {
refreshCache()
}
if checkDBtime() {
if err := pushDatabase(); err != nil {
log.Printf("Error pushing cache to database: %v\n", err)
}
}
}
}
// Refreshes the cache.
func refreshCache() {
// Iterate over the registry and
// update each individual user.
for k := range twtxtCache.Reg {
err := twtxtCache.UpdateUser(k)
if err != nil {
log.Printf("%v\n", err)
continue
}
}
// Re-scrape all the remote registries
// to see if they have any new users
// to add locally.
for _, v := range remoteRegistries.List {
err := twtxtCache.ScrapeRemoteRegistry(v)
if err != nil {
log.Printf("Error while refreshing local copy of remote registry user data: %v\n", err)
}
}
confObj.Mu.Lock()
confObj.LastCache = time.Now()
confObj.Mu.Unlock()
}
// Pushes the registry's cache data to a local
// database for safe keeping.
func pushDatabase() error {
// Acquire the database from the aether.
// goleveldb is concurrency-safe, so we
// can immediately push it back into the
// channel for other functions to use.
db := <-dbChan
dbChan <- db
// Create a batch write job so it can
// be done at one time rather than
// per entry.
twtxtCache.Mu.RLock()
var dbBasket = &leveldb.Batch{}
for k, v := range twtxtCache.Reg {
dbBasket.Put([]byte(k+"*Nick"), []byte(v.Nick))
dbBasket.Put([]byte(k+"*URL"), []byte(v.URL))
dbBasket.Put([]byte(k+"*IP"), []byte(v.IP.String()))
dbBasket.Put([]byte(k+"*Date"), []byte(v.Date))
for i, e := range v.Status {
rfc := i.Format(time.RFC3339)
dbBasket.Put([]byte(k+"*Status*"+rfc), []byte(e))
}
}
twtxtCache.Mu.RUnlock()
// Save our list of remote registries to scrape.
for k, v := range remoteRegistries.List {
dbBasket.Put([]byte("remote*"+string(k)), []byte(v))
}
// Execute the batch job.
if err := db.Write(dbBasket, nil); err != nil {
return err
}
// Update the last push time for
// our timer/watch function to
// reference.
confObj.Mu.Lock()
confObj.LastPush = time.Now()
confObj.Mu.Unlock()
return nil
}
// Pulls registry data from the DB on startup.
// Iterates over the database one entry at a time.
func pullDatabase() {
// Acquire the database from the aether.
// goleveldb is concurrency-safe, so we
// can immediately push it back into the
// channel for other functions to use.
db := <-dbChan
dbChan <- db
iter := db.NewIterator(nil, nil)
// Read the database entry-by-entry
for iter.Next() {
key := string(iter.Key())
val := string(iter.Value())
split := strings.Split(key, "*")
urls := string(split[0])
field := string(split[1])
if urls != "remote" {
// Start with an empty Data struct. If
// there's already one in the cache, pull
// it and use it instead.
data := registry.NewUserData()
twtxtCache.Mu.RLock()
if _, ok := twtxtCache.Reg[urls]; ok {
data = twtxtCache.Reg[urls]
}
twtxtCache.Mu.RUnlock()
switch field {
case "IP":
data.IP = net.ParseIP(val)
case "Nick":
data.Nick = val
case "URL":
data.URL = val
case "Date":
data.Date = val
case "Status":
// If we're looking at a Status entry in the DB,
// parse the time then add it to the TimeMap under
// data.Status
thetime, err := time.Parse(time.RFC3339, split[2])
if err != nil {
log.Printf("%v\n", err)
}
data.Status[thetime] = val
}
// Push the data struct (back) into
// the cache.
twtxtCache.Mu.Lock()
twtxtCache.Reg[urls] = data
twtxtCache.Mu.Unlock()
} else {
// If we've come across an entry for
// a remote twtxt registry to scrape,
// add it to our list.
remoteRegistries.Mu.Lock()
remoteRegistries.List = append(remoteRegistries.List, val)
remoteRegistries.Mu.Unlock()
}
}
iter.Release()
err := iter.Error()
if err != nil {
log.Printf("Error while pulling DB into registry cache: %v\n", err)
}
}
// pingAssets checks if the local static assets
// need to be re-cached. If they do, they are
// pulled back into memory from disk.
func pingAssets() {
cssStat, err := os.Stat("assets/style.css")
if err != nil {
log.Printf("%v\n", err)
}
indexStat, err := os.Stat("assets/tmpl/index.html")
if err != nil {
log.Printf("%v\n", err)
}
indexMod := staticCache.indexMod
cssMod := staticCache.cssMod
if !indexMod.Equal(indexStat.ModTime()) {
var err error
tmpls, err = template.ParseFiles("assets/tmpl/index.html")
if err != nil {
log.Printf("%v\n", err)
}
var b []byte
buf := bytes.NewBuffer(b)
confObj.Mu.RLock()
err = tmpls.ExecuteTemplate(buf, "index.html", confObj.Instance)
confObj.Mu.RUnlock()
if err != nil {
log.Printf("%v\n", err)
}
staticCache.index = buf.Bytes()
staticCache.indexMod = indexStat.ModTime()
}
if !cssMod.Equal(cssStat.ModTime()) {
css, err := ioutil.ReadFile("assets/style.css")
if err != nil {
log.Printf("%v\n", err)
}
staticCache.css = css
staticCache.cssMod = cssStat.ModTime()
}
}