package file import ( "bufio" "bytes" "errors" "fmt" "io" "io/fs" "os" "path" "path/filepath" "strings" "time" "git.andreafazzi.eu/andrea/probo/client" "git.andreafazzi.eu/andrea/probo/models" "github.com/go-yaml/yaml" ) func (s *FileProboCollectorStore) GetQuizzesDir() string { return s.quizzesDir } 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) ReadAllQuizzes() ([]*models.Quiz, error) { return s.memoryStore.ReadAllQuizzes() } 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 } 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) if err != nil { return nil, err } if updated { // Update and re-index only if quiz hash is changed err = s.createOrUpdateMarkdownFile(quiz) if err != nil { return nil, err } err = s.Reindex() if err != nil { return nil, err } } return s.memoryStore.ReadQuizByHash(quiz.Hash) } func (s *FileProboCollectorStore) DeleteQuiz(r *client.DeleteQuizRequest) (*models.Quiz, error) { quiz, err := s.memoryStore.DeleteQuiz(&client.DeleteQuizRequest{ID: r.ID}) if err != nil { return nil, err } path, err := s.GetQuizPath(quiz) 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 } func (s *FileProboCollectorStore) GetQuizPath(quiz *models.Quiz) (string, error) { if quiz == nil { return "", errors.New("Quiz object passed as argument is nil!") } s.lock.RLock() defer s.lock.RUnlock() path, ok := s.quizzesPaths[quiz.ID] if !ok { return "", errors.New(fmt.Sprintf("Path not found for quiz ID %v", quiz.ID)) } return path, nil } func MarkdownFromQuiz(quiz *models.Quiz) (string, error) { if quiz.Question == nil { return "", errors.New("Quiz should contain a question but it wasn't provided.") } if len(quiz.Answers) < 2 { return "", errors.New("Quiz should contain at least 2 answers but none was provided.") } if quiz.Correct == nil { return "", errors.New("Quiz should contain a correct answer but not was provided.") } correctAnswer := "* " + quiz.Correct.Text var otherAnswers string for _, answer := range quiz.Answers { if quiz.Correct.ID != answer.ID { otherAnswers += "* " + answer.Text + "\n" } } markdown := quiz.Question.Text + "\n\n" + correctAnswer + "\n" + otherAnswers return markdown, nil } 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") 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 == "" { return nil, nil, fmt.Errorf("Question text should not be empty.") } if len(answers) < 2 { return nil, nil, fmt.Errorf("Number of answers should be at least 2 but parsed answers are %d.", len(answers)) } question := &client.Question{Text: questionText} quiz := &client.Quiz{Question: question, Answers: answers} return quiz, meta, nil } func (s *FileProboCollectorStore) ReadMetaHeaderFromFile(filename string) (*models.Meta, error) { data, err := os.ReadFile(path.Join(s.quizzesDir, filename)) 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 { _, err := writeMetaHeader(path.Join(s.quizzesDir, filename), meta) if err != nil { return nil, err } } return meta, nil } func (s *FileProboCollectorStore) reindexQuizzes() error { files, err := os.ReadDir(s.quizzesDir) if err != nil { return err } markdownFiles := make([]fs.DirEntry, 0) for _, file := range files { filename := file.Name() if !file.IsDir() && strings.HasSuffix(filename, ".md") { markdownFiles = append(markdownFiles, file) } } if len(markdownFiles) == 0 { return fmt.Errorf("The directory is empty.") } for _, file := range markdownFiles { filename := file.Name() fullPath := filepath.Join(s.quizzesDir, filename) content, err := os.ReadFile(fullPath) if err != nil { return err } quiz, meta, err := QuizFromMarkdown(string(content)) if err != nil { return err } q, err := s.memoryStore.CreateQuiz(&client.CreateUpdateQuizRequest{ Quiz: quiz, Meta: meta, }) if err != nil { return err } if meta == nil { s.WriteMetaHeaderToFile(filename, &models.Meta{ ID: q.ID, CreatedAt: time.Now(), }) } s.SetQuizPath(q, fullPath) } return nil } func (s *FileProboCollectorStore) removeMetaFromFile(filename string) (*models.Meta, error) { file, err := os.Open(path.Join(s.quizzesDir, filename)) 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 } file, err = os.Create(path.Join(s.quizzesDir, filename)) if err != nil { return nil, err } defer file.Close() _, err = io.Copy(file, &buffer) if err != nil { return nil, err } return &meta, nil } func (s *FileProboCollectorStore) createOrUpdateMarkdownFile(quiz *models.Quiz) error { markdown, err := MarkdownFromQuiz(quiz) if err != nil { return err } fn, _ := s.GetQuizPath(quiz) if fn == "" { fn = filepath.Join(s.quizzesDir, fmt.Sprintf("quiz_%v.%s", quiz.ID, "md")) } file, err := os.Create(fn) if err != nil { return err } defer file.Close() markdownWithMetaHeader, err := addMetaHeaderToMarkdown(markdown, &quiz.Meta) if err != nil { return err } _, err = file.Write([]byte(markdownWithMetaHeader)) if err != nil { return err } s.SetQuizPath(quiz, fn) return nil } 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 }