/* SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine Copyright (C) 2020-2023 Sergey Matveev This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package main import ( "encoding/gob" "io" "os" "sort" "strconv" "time" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "go.stargrave.org/sgblog" ) type TopicsCache map[string][]plumbing.Hash type TopicsCacheState struct { Top plumbing.Hash Cache TopicsCache } func (tc TopicsCache) Topics() []string { topics := make([]string, 0, len(tc)) for t := range tc { topics = append(topics, t) } sort.Strings(topics) return topics } func getTopicsCache(cfg *Cfg, repoLog object.CommitIter) (TopicsCache, error) { cache := TopicsCache(make(map[string][]plumbing.Hash)) if topicsTree == nil { return cache, nil } top := topicsRef.Hash() if cfg.TopicsCachePath != "" { fd, err := os.Open(cfg.TopicsCachePath) if err != nil { goto NoCache } dec := gob.NewDecoder(fd) var cacheState TopicsCacheState err = dec.Decode(&cacheState) fd.Close() if err != nil { goto NoCache } if cacheState.Top == top { return cacheState.Cache, nil } } NoCache: for { commit, err := repoLog.Next() if err != nil { break } for _, topic := range sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)) { cache[topic] = append(cache[topic], commit.Hash) } } if cfg.TopicsCachePath != "" { // Assume that probability of suffix collision is negligible suffix := strconv.FormatInt(time.Now().UnixNano()+int64(os.Getpid()), 16) tmpPath := cfg.TopicsCachePath + suffix fd, err := os.OpenFile( tmpPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, os.FileMode(0666), ) if err != nil { return cache, err } enc := gob.NewEncoder(fd) err = enc.Encode(&TopicsCacheState{top, cache}) if err != nil { os.Remove(tmpPath) fd.Close() return cache, err } if err = fd.Sync(); err != nil { os.Remove(tmpPath) fd.Close() return cache, err } if err = fd.Close(); err != nil { os.Remove(tmpPath) return cache, err } if err = os.Rename(tmpPath, cfg.TopicsCachePath); err != nil { os.Remove(tmpPath) return cache, err } } return cache, nil } type HashesIter struct { hashes []plumbing.Hash } func (s *HashesIter) Next() (*object.Commit, error) { if len(s.hashes) == 0 { return nil, io.EOF } var h plumbing.Hash h, s.hashes = s.hashes[0], s.hashes[1:] return repo.CommitObject(h) }