probo/store/file/file.go

667 lines
13 KiB
Go
Raw Normal View History

2023-06-28 17:21:59 +02:00
package file
import (
2023-09-22 10:29:10 +02:00
"bufio"
"bytes"
2023-10-07 11:43:12 +02:00
"encoding/json"
2023-06-28 17:21:59 +02:00
"errors"
"fmt"
2023-09-22 10:29:10 +02:00
"io"
2023-06-28 17:21:59 +02:00
"io/fs"
"io/ioutil"
"os"
2023-09-22 10:29:10 +02:00
"path"
2023-06-28 17:21:59 +02:00
"path/filepath"
"strings"
2023-07-10 13:23:46 +02:00
"sync"
"time"
2023-06-28 17:21:59 +02:00
"git.andreafazzi.eu/andrea/probo/client"
"git.andreafazzi.eu/andrea/probo/hasher/sha256"
"git.andreafazzi.eu/andrea/probo/models"
"git.andreafazzi.eu/andrea/probo/store/memory"
2023-09-22 10:29:10 +02:00
"github.com/go-yaml/yaml"
2023-06-28 17:21:59 +02:00
)
2023-10-07 11:43:12 +02:00
var (
ErrorMetaHeaderIsNotPresent = errors.New("Meta header was not found in file.")
DefaultQuizzesDir = "quizzes"
DefaultCollectionsDir = "collections"
)
2023-09-22 10:29:10 +02:00
2023-06-28 17:21:59 +02:00
type FileProboCollectorStore struct {
Dir string
memoryStore *memory.MemoryProboCollectorStore
2023-10-07 11:43:12 +02:00
quizzesPaths map[string]string
collectionsPaths map[string]string
quizzesDir string
collectionsDir string
2023-07-10 13:23:46 +02:00
// A mutex is used to synchronize read/write access to the map
lock sync.RWMutex
2023-06-28 17:21:59 +02:00
}
func NewFileProboCollectorStore(dirname string) (*FileProboCollectorStore, error) {
s := new(FileProboCollectorStore)
2023-07-10 13:23:46 +02:00
s.Dir = dirname
2023-10-07 11:43:12 +02:00
s.quizzesDir = filepath.Join(s.Dir, DefaultQuizzesDir)
s.collectionsDir = filepath.Join(s.Dir, DefaultCollectionsDir)
2023-07-10 13:23:46 +02:00
err := s.Reindex()
2023-06-28 17:21:59 +02:00
if err != nil {
return nil, err
}
2023-07-10 13:23:46 +02:00
return s, nil
}
2023-10-07 11:43:12 +02:00
func (s *FileProboCollectorStore) GetQuizzesDir() string {
return s.quizzesDir
}
func (s *FileProboCollectorStore) GetCollectionsDir() string {
return s.collectionsDir
}
func (s *FileProboCollectorStore) reindexQuizzes() error {
files, err := ioutil.ReadDir(s.quizzesDir)
2023-07-10 13:23:46 +02:00
if err != nil {
return err
}
2023-06-28 17:21:59 +02:00
markdownFiles := make([]fs.FileInfo, 0)
for _, file := range files {
filename := file.Name()
if !file.IsDir() && strings.HasSuffix(filename, ".md") {
markdownFiles = append(markdownFiles, file)
}
}
if len(markdownFiles) == 0 {
2023-07-10 13:23:46 +02:00
return fmt.Errorf("The directory is empty.")
2023-06-28 17:21:59 +02:00
}
for _, file := range markdownFiles {
filename := file.Name()
2023-10-07 11:43:12 +02:00
fullPath := filepath.Join(s.quizzesDir, filename)
2023-06-28 17:21:59 +02:00
content, err := os.ReadFile(fullPath)
if err != nil {
2023-07-10 13:23:46 +02:00
return err
2023-06-28 17:21:59 +02:00
}
2023-09-22 10:29:10 +02:00
quiz, meta, err := QuizFromMarkdown(string(content))
2023-06-28 17:21:59 +02:00
if err != nil {
2023-07-10 13:23:46 +02:00
return err
2023-06-28 17:21:59 +02:00
}
2023-07-10 13:23:46 +02:00
q, err := s.memoryStore.CreateQuiz(&client.CreateUpdateQuizRequest{
2023-06-28 17:21:59 +02:00
Quiz: quiz,
2023-09-22 10:29:10 +02:00
Meta: meta,
2023-06-28 17:21:59 +02:00
})
2023-07-10 13:23:46 +02:00
if err != nil {
return err
}
2023-06-28 17:21:59 +02:00
2023-09-22 10:29:10 +02:00
if meta == nil {
s.WriteMetaHeaderToFile(filename, &models.Meta{
ID: q.ID,
CreatedAt: time.Now(),
})
}
2023-10-07 11:43:12 +02:00
s.SetQuizPath(q, fullPath)
}
return nil
}
func (s *FileProboCollectorStore) reindexCollections() error {
files, err := ioutil.ReadDir(s.collectionsDir)
if err != nil {
return err
}
jsonFiles := make([]fs.FileInfo, 0)
for _, file := range files {
filename := file.Name()
if !file.IsDir() && strings.HasSuffix(filename, ".json") {
jsonFiles = append(jsonFiles, file)
}
}
for _, file := range jsonFiles {
filename := file.Name()
fullPath := filepath.Join(s.collectionsDir, filename)
content, err := os.ReadFile(fullPath)
if err != nil {
return err
}
var clientCollection *client.Collection
err = json.Unmarshal(content, &clientCollection)
if err != nil {
return err
}
collection, err := s.memoryStore.CreateCollection(&client.CreateUpdateCollectionRequest{
Collection: clientCollection,
})
if err != nil {
return err
}
s.SetCollectionPath(collection, fullPath)
}
return nil
}
func (s *FileProboCollectorStore) Reindex() error {
s.memoryStore = memory.NewMemoryProboCollectorStore(
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
)
s.quizzesPaths = make(map[string]string)
s.collectionsPaths = make(map[string]string)
err := s.reindexQuizzes()
if err != nil {
return err
}
err = s.reindexCollections()
if err != nil {
return err
2023-07-10 13:23:46 +02:00
}
2023-06-28 17:21:59 +02:00
2023-07-10 13:23:46 +02:00
return nil
2023-06-28 17:21:59 +02:00
}
func (s *FileProboCollectorStore) ReadAllQuizzes() ([]*models.Quiz, error) {
return s.memoryStore.ReadAllQuizzes()
}
2023-06-28 17:21:59 +02:00
func (s *FileProboCollectorStore) CreateQuiz(r *client.CreateUpdateQuizRequest) (*models.Quiz, error) {
quiz, err := s.memoryStore.CreateQuiz(r)
if err != nil {
return nil, err
}
err = s.createOrUpdateMarkdownFile(quiz)
if err != nil {
return nil, err
}
2023-07-10 13:23:46 +02:00
err = s.Reindex()
if err != nil {
return nil, err
}
return s.memoryStore.ReadQuizByHash(quiz.Hash)
}
func (s *FileProboCollectorStore) UpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, error) {
quiz, updated, err := s.memoryStore.UpdateQuiz(r, id)
2023-06-28 17:21:59 +02:00
if err != nil {
return nil, err
}
if updated { // Update and re-index only if quiz hash is changed
2023-07-12 15:57:10 +02:00
err = s.createOrUpdateMarkdownFile(quiz)
if err != nil {
return nil, err
}
2023-07-10 13:23:46 +02:00
2023-07-12 15:57:10 +02:00
err = s.Reindex()
if err != nil {
return nil, err
}
2023-07-10 13:23:46 +02:00
}
return s.memoryStore.ReadQuizByHash(quiz.Hash)
}
2023-09-01 11:48:09 +02:00
func (s *FileProboCollectorStore) DeleteQuiz(r *client.DeleteQuizRequest) (*models.Quiz, error) {
quiz, err := s.memoryStore.DeleteQuiz(r.ID)
if err != nil {
return nil, err
}
2023-10-07 11:43:12 +02:00
path, err := s.GetQuizPath(quiz)
2023-09-01 11:48:09 +02:00
if err != nil {
return nil, err
}
err = os.Remove(path)
if err != nil {
return nil, err
}
err = s.Reindex()
if err != nil {
return nil, err
}
return quiz, nil
}
2023-10-07 11:43:12 +02:00
func (s *FileProboCollectorStore) GetQuizPath(quiz *models.Quiz) (string, error) {
if quiz == nil {
return "", errors.New("Quiz object passed as argument is nil!")
}
2023-07-10 13:23:46 +02:00
s.lock.RLock()
defer s.lock.RUnlock()
2023-10-07 11:43:12 +02:00
path, ok := s.quizzesPaths[quiz.ID]
2023-07-10 13:23:46 +02:00
if !ok {
return "", errors.New(fmt.Sprintf("Path not found for quiz ID %v", quiz.ID))
}
return path, nil
}
2023-10-07 11:43:12 +02:00
func (s *FileProboCollectorStore) GetCollectionPath(collection *models.Collection) (string, error) {
s.lock.RLock()
defer s.lock.RUnlock()
path, ok := s.collectionsPaths[collection.ID]
if !ok {
return "", errors.New(fmt.Sprintf("Path not found for collection ID %v", collection.ID))
}
return path, nil
}
func (s *FileProboCollectorStore) SetQuizPath(quiz *models.Quiz, path string) string {
s.lock.Lock()
defer s.lock.Unlock()
s.quizzesPaths[quiz.ID] = path
return path
}
func (s *FileProboCollectorStore) SetCollectionPath(collection *models.Collection, path string) string {
2023-07-10 13:23:46 +02:00
s.lock.Lock()
defer s.lock.Unlock()
2023-10-07 11:43:12 +02:00
s.collectionsPaths[collection.ID] = path
2023-07-10 13:23:46 +02:00
return path
2023-06-28 17:21:59 +02:00
}
func MarkdownFromQuiz(quiz *models.Quiz) (string, error) {
if quiz.Question == nil {
return "", errors.New("Quiz should contain a question but it wasn't provided.")
}
2023-07-10 13:23:46 +02:00
if len(quiz.Answers) < 2 {
2023-06-28 17:21:59 +02:00
return "", errors.New("Quiz should contain at least 2 answers but none was provided.")
}
if quiz.Correct == nil {
2023-07-10 13:23:46 +02:00
return "", errors.New("Quiz should contain a correct answer but not was provided.")
2023-06-28 17:21:59 +02:00
}
correctAnswer := "* " + quiz.Correct.Text
var otherAnswers string
for _, answer := range quiz.Answers {
2023-07-12 10:53:53 +02:00
if quiz.Correct.ID != answer.ID {
2023-07-10 13:23:46 +02:00
otherAnswers += "* " + answer.Text + "\n"
}
2023-06-28 17:21:59 +02:00
}
markdown := quiz.Question.Text + "\n\n" + correctAnswer + "\n" + otherAnswers
return markdown, nil
}
2023-09-22 10:29:10 +02:00
func QuizFromMarkdown(markdown string) (*client.Quiz, *models.Meta, error) {
meta, remainingMarkdown, err := parseMetaHeaderFromMarkdown(markdown)
if err != nil {
return nil, nil, err
}
lines := strings.Split(remainingMarkdown, "\n")
2023-06-28 17:21:59 +02:00
questionText := ""
answers := []*client.Answer{}
for _, line := range lines {
if strings.HasPrefix(line, "*") {
answerText := strings.TrimPrefix(line, "* ")
correct := len(answers) == 0
answer := &client.Answer{Text: answerText, Correct: correct}
answers = append(answers, answer)
} else {
if questionText != "" {
questionText += "\n"
}
questionText += line
}
}
questionText = strings.TrimRight(questionText, "\n")
if questionText == "" {
2023-09-22 10:29:10 +02:00
return nil, nil, fmt.Errorf("Question text should not be empty.")
2023-06-28 17:21:59 +02:00
}
if len(answers) < 2 {
2023-09-22 10:29:10 +02:00
return nil, nil, fmt.Errorf("Number of answers should be at least 2 but parsed answers are %d.", len(answers))
2023-06-28 17:21:59 +02:00
}
question := &client.Question{Text: questionText}
quiz := &client.Quiz{Question: question, Answers: answers}
2023-09-22 10:29:10 +02:00
return quiz, meta, nil
}
func (s *FileProboCollectorStore) ReadMetaHeaderFromFile(filename string) (*models.Meta, error) {
2023-10-07 11:43:12 +02:00
data, err := ioutil.ReadFile(path.Join(s.quizzesDir, filename))
2023-09-22 10:29:10 +02:00
if err != nil {
return nil, err
}
meta, _, err := parseMetaHeaderFromMarkdown(string(data))
if err != nil {
return nil, err
}
return meta, nil
}
func (s *FileProboCollectorStore) WriteMetaHeaderToFile(filename string, meta *models.Meta) (*models.Meta, error) {
readMeta, err := s.ReadMetaHeaderFromFile(filename)
if err != nil {
return nil, err
}
if readMeta == nil {
2023-10-07 11:43:12 +02:00
_, err := writeMetaHeader(path.Join(s.quizzesDir, filename), meta)
2023-09-22 10:29:10 +02:00
if err != nil {
return nil, err
}
}
return meta, nil
}
2023-10-07 11:43:12 +02:00
func (s *FileProboCollectorStore) CreateCollection(r *client.CreateUpdateCollectionRequest) (*models.Collection, error) {
collection, err := s.memoryStore.CreateCollection(r)
if err != nil {
return nil, err
}
err = s.createOrUpdateCollectionFile(collection)
if err != nil {
return nil, err
}
return s.memoryStore.ReadCollectionByID(collection.ID)
}
2023-09-22 10:29:10 +02:00
func (s *FileProboCollectorStore) removeMetaFromFile(filename string) (*models.Meta, error) {
2023-10-07 11:43:12 +02:00
file, err := os.Open(path.Join(s.quizzesDir, filename))
2023-09-22 10:29:10 +02:00
if err != nil {
return nil, err
}
defer file.Close()
var buffer bytes.Buffer
reader := bufio.NewReader(file)
var meta models.Meta
var line string
var sb strings.Builder
for {
line, err = reader.ReadString('\n')
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
if strings.TrimSpace(line) == "---" {
break
}
}
for {
line, err = reader.ReadString('\n')
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
if strings.TrimSpace(line) == "---" {
break
}
sb.WriteString(line)
}
err = yaml.Unmarshal([]byte(sb.String()), &meta)
if err != nil {
return nil, err
}
_, err = io.Copy(&buffer, reader)
if err != nil {
return nil, err
}
2023-10-07 11:43:12 +02:00
file, err = os.Create(path.Join(s.quizzesDir, filename))
2023-09-22 10:29:10 +02:00
if err != nil {
return nil, err
}
defer file.Close()
_, err = io.Copy(file, &buffer)
if err != nil {
return nil, err
}
return &meta, nil
2023-06-28 17:21:59 +02:00
}
func (s *FileProboCollectorStore) createOrUpdateMarkdownFile(quiz *models.Quiz) error {
2023-06-28 17:21:59 +02:00
markdown, err := MarkdownFromQuiz(quiz)
if err != nil {
return err
}
2023-07-10 13:23:46 +02:00
2023-10-07 11:43:12 +02:00
fn, _ := s.GetQuizPath(quiz)
if fn == "" {
2023-10-07 11:43:12 +02:00
fn = filepath.Join(s.quizzesDir, fmt.Sprintf("quiz_%v.%s", quiz.ID, "md"))
}
2023-07-10 13:23:46 +02:00
file, err := os.Create(fn)
if err != nil {
return err
}
2023-10-07 11:43:12 +02:00
defer file.Close()
2023-06-28 17:21:59 +02:00
2023-09-22 10:29:10 +02:00
markdownWithMetaHeader, err := addMetaHeaderToMarkdown(markdown, &quiz.Meta)
if err != nil {
return err
}
_, err = file.Write([]byte(markdownWithMetaHeader))
2023-06-28 17:21:59 +02:00
if err != nil {
return err
}
2023-10-07 11:43:12 +02:00
s.SetQuizPath(quiz, fn)
2023-07-10 13:23:46 +02:00
2023-06-28 17:21:59 +02:00
return nil
}
2023-09-22 10:29:10 +02:00
2023-10-07 11:43:12 +02:00
func (s *FileProboCollectorStore) createOrUpdateCollectionFile(collection *models.Collection) error {
json, err := json.Marshal(collection)
if err != nil {
return err
}
fn, _ := s.GetCollectionPath(collection)
if fn == "" {
fn = filepath.Join(s.collectionsDir, fmt.Sprintf("collection_%v.%s", collection.ID, "json"))
}
file, err := os.Create(fn)
if err != nil {
return err
}
defer file.Close()
_, err = file.Write([]byte(json))
if err != nil {
return err
}
s.SetCollectionPath(collection, fn)
return nil
}
2023-09-22 10:29:10 +02:00
func addMetaHeaderToMarkdown(content string, meta *models.Meta) (string, error) {
var buffer bytes.Buffer
header, err := yaml.Marshal(meta)
if err != nil {
return "", err
}
_, err = buffer.WriteString("---\n" + string(header) + "---\n")
if err != nil {
return "", err
}
_, err = buffer.WriteString(content)
if err != nil {
return "", err
}
return buffer.String(), nil
}
func parseMetaHeaderFromMarkdown(markdown string) (*models.Meta, string, error) {
reader := strings.NewReader(markdown)
var sb strings.Builder
var line string
var err error
for {
line, err = readLine(reader)
if err != nil {
if err == io.EOF {
break
}
return nil, "", err
}
if strings.TrimSpace(line) == "---" {
break
}
}
for {
line, err = readLine(reader)
if err != nil {
if err == io.EOF {
break
}
return nil, "", err
}
if strings.TrimSpace(line) == "---" {
break
}
sb.WriteString(line)
}
if sb.String() == "" {
return nil, markdown, nil
}
var meta models.Meta
err = yaml.Unmarshal([]byte(sb.String()), &meta)
if err != nil {
return nil, markdown, err
}
remainingMarkdown := markdown[strings.Index(markdown, "---\n"+sb.String()+"---\n")+len("---\n"+sb.String()+"---\n"):]
return &meta, remainingMarkdown, nil
}
func readLine(reader *strings.Reader) (string, error) {
var sb strings.Builder
for {
r, _, err := reader.ReadRune()
if err != nil {
if err == io.EOF {
return sb.String(), io.EOF
}
return "", err
}
sb.WriteRune(r)
if r == '\n' {
break
}
}
return sb.String(), nil
}
func writeMetaHeader(filename string, meta *models.Meta) (*models.Meta, error) {
// Apri il file in lettura
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
// Crea un buffer in memoria
var buffer bytes.Buffer
// Scrivi l'intestazione YAML nel buffer
header, err := yaml.Marshal(meta)
if err != nil {
return nil, err
}
_, err = buffer.WriteString("---\n" + string(header) + "---\n")
if err != nil {
return nil, err
}
// Copia il contenuto del file originale nel buffer
_, err = io.Copy(&buffer, file)
if err != nil {
return nil, err
}
// Riapri il file in scrittura
file, err = os.Create(filename)
if err != nil {
return nil, err
}
defer file.Close()
// Scrivi il contenuto del buffer nel file
_, err = io.Copy(file, &buffer)
if err != nil {
return nil, err
}
return meta, nil
}