utils.cache.go

  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
// This file contains code related to storing documents e.g. activities, notes, shares, likes etc.
// All documents are stored in a set of structured folder organized by type, user and document id
// We call these "caches" to reflect that they are designed to be somewhat-ephemeral
// as such we expect instance admins to write scripts to purge older files arbitrarily and nothing in the
// is written to expect the existence of any particular file after it is written
package main

import (
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// CacheCount returns the number of documents stored in a particular cache, this is mostly
// used displaying the number of likes/shares a post has, but can also be used for post count
// follower count and most other information like that.
func CacheCount(user *User, store string, substore string) int {
	cacheDir := filepath.Join(".", store, strings.TrimPrefix(user.UserName, "@"), substore)
	if files, err := os.ReadDir(cacheDir); err == nil {
		return len(files)
	} else {
		return 0
	}
}

// CacheId is a utility function that calculates the file system path address for a cache entry
func CacheId(user *User, store string, substore string, key string) string {
	cacheDir := filepath.Join(".", store, strings.TrimPrefix(user.UserName, "@"), substore)
	os.MkdirAll(cacheDir, 0755)
	keyHash := sha256.Sum256([]byte(key))
	b64 := fmt.Sprintf("%x", keyHash)
	path := filepath.Join(filepath.Join(cacheDir, b64))
	return path
}

// Stores a raw blob in a specific cache store e.g. "followers" or "replies"
func CacheData(user *User, store string, substore string, key string, data []byte) {
	os.WriteFile(CacheId(user, store, substore, key), data, 0644)
}

// Removes a raw blob in a specific cache store e.g. "followers" or "replies"
func DeleteFromCache(user *User, store string, substore string, key string) {
	os.Remove(CacheId(user, store, substore, key))
}

// Loads a single item from the cache
func FromCache(user *User, store string, substore string, key string) ([]byte, error) {
	return os.ReadFile(CacheId(user, store, substore, key))
}

// Walks a cache so it can be processed by the calling function
func WalkCache(user *User, store string, substore string, onRecord func(id string, data []byte)) {
	cacheDir := filepath.Join(".", store, strings.TrimPrefix(user.UserName, "@"), substore)
	items, _ := os.ReadDir(cacheDir)
	for _, item := range items {
		if !item.IsDir() {
			path := filepath.Join(cacheDir, item.Name())
			data, err := os.ReadFile(path)
			if err == nil {
				onRecord(path, data)
			}
		}
	}
}

// load a note from the cache, if it exists and return it
func (user *User) NoteFromCache(id string) *NoteMeta {
	if data, err := FromCache(user, "notes", "", id); err == nil {
		note := &NoteMeta{}
		if err := json.Unmarshal(data, note); err == nil {
			note.created, _ = time.Parse("2006-01-02T15:04:05Z", note.Published)
			return note
		}
	}
	return nil
}

// Given a cache reference, return a Note if it exists.
// This can be used for both activities and note references.
func (user *User) NoteFromRef(ref string) *NoteMeta {
	if data, err := os.ReadFile(ref); err == nil {
		if strings.HasPrefix(ref, "notes") {
			targetNote := &NoteMeta{}
			if err := json.Unmarshal(data, targetNote); err == nil {
				// note: we can safely use attributed to here as we assume instance-created notes
				// will never have false attribution (and will always be attributed to the correct user)
				targetNote.actor = targetNote.AttributedTo
				return targetNote
			}
		} else {
			targetNote := &PostMeta{}
			if err := json.Unmarshal(data, targetNote); err == nil {
				targetNote.Object.actor = targetNote.Actor
				return &targetNote.Object
			}
		}
	}
	return nil
}