obs2hugo/main.go
Óscar M. Lage 6a56a2f9f2 Feat: Add support for 'build: true' tag in frontmatter
This feature allows triggering the execution of the `build_action` command specified in
the `.ini` file when adding `build: true` to the frontmatter of the modified file. Upon
detecting this tag, the specified command will be executed automatically, facilitating
the automation of build processes in response to specific file modifications
2024-04-10 12:02:42 +02:00

433 lines
11 KiB
Go

package main
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/fsnotify/fsnotify"
"github.com/gosimple/slug"
"gopkg.in/ini.v1"
)
func main() {
// Read the config file
home, err := os.UserHomeDir()
if err != nil {
log.Fatalln(err)
}
config, err := ini.Load(home + "/.config/obs2hugo/obs2hugo.ini")
if err != nil {
config, err = ini.Load(home + "/.config/obs2hugo.ini")
if err != nil {
log.Fatalln(err)
}
}
// Hugo build action
buildAction := config.Section("").Key("build_action").String()
// Watcher directories
watcherDirs := config.Section("").Key("watcher_dirs").String()
// Hugo content directory
destDirs := config.Section("").Key("hugo_dirs").String()
watcherDirsList := strings.Split(watcherDirs, ":")
destDirsList := strings.Split(destDirs, ":")
if len(watcherDirsList) != len(destDirsList) {
fmt.Println("Error: watcher_dirs and hugo_dirs must have the same number of elements")
return
}
// Creating watcher
watcher, err := fsnotify.NewWatcher()
if err != nil {
fmt.Println("Error creating the watcher:", err)
return
}
defer watcher.Close()
// Add directories to the watcher
for _, dir := range watcherDirsList {
err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
fmt.Println("Error accessing to the directory:", err)
return err
}
if info.IsDir() {
return watcher.Add(path)
}
return nil
})
if err != nil {
fmt.Println("Error adding directory to the watcher:", err)
return
}
}
// Watcher event loop
for {
select {
case event := <-watcher.Events:
switch {
case event.Op&fsnotify.Create == fsnotify.Create:
fmt.Println("New file:", event.Name)
// TBD
case event.Op&fsnotify.Write == fsnotify.Write:
fmt.Println("File modified:", event.Name)
triggerDir := getTriggerDirectory(event.Name, watcherDirsList)
if triggerDir == "" {
fmt.Println("Error: Unable to determine trigger directory for event:", event.Name)
continue
}
newDestDir := getDestinationDirectory(triggerDir, watcherDirsList, destDirsList)
if newDestDir == "" {
fmt.Println("Error: Unable to determine destination directory for trigger directory:", triggerDir)
continue
}
processModifiedFile(event.Name, triggerDir, newDestDir)
// Check if the file contains the "build: true" tag in the frontmatter
hasBuildTag, err := hasBuildTag(event.Name)
if err != nil {
fmt.Println("Error checking build tag in frontmatter:", err)
continue
}
// If the file contains the "build: true" tag, execute the build action specified in the .ini file
if hasBuildTag && buildAction != "" {
// Remove the "build: true" tag from the frontmatter
err = removeBuildTag(event.Name)
if err != nil {
fmt.Println("Error removing build tag from frontmatter:", err)
return
}
// Execute build action
err := executeBuildAction(buildAction)
if err != nil {
fmt.Println("Error executing build action:", err)
continue
}
}
case event.Op&fsnotify.Remove == fsnotify.Remove:
fmt.Println("File deleted:", event.Name)
// TBD
case event.Op&fsnotify.Rename == fsnotify.Rename:
fmt.Println("File renamed:", event.Name)
// TBD
}
case err := <-watcher.Errors:
fmt.Println("Watcher error:", err)
}
}
}
// Function to determine which watcher directory triggered the event
func getTriggerDirectory(eventPath string, dirs []string) string {
for _, dir := range dirs {
if strings.HasPrefix(eventPath, dir) {
return dir
}
}
return ""
}
// Function to determine the destination directory for the trigger directory
func getDestinationDirectory(triggerDir string, watcherDirsList, destDirsList []string) string {
for i, dir := range watcherDirsList {
if dir == triggerDir {
return destDirsList[i]
}
}
return ""
}
func processModifiedFile(filePath, directory, destDir string) {
// Slugified file name without extension
fileName := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath))
slug := slug.Make(fileName)
// Creating destination directory
newDir := filepath.Join(destDir, slug)
err := os.MkdirAll(newDir, os.ModePerm)
if err != nil {
fmt.Println("Error creating directory:", err)
return
}
// Copy modified file to the new destination
newFilePath := filepath.Join(newDir, "index.md")
// Check if the file already has frontmatter
hasFrontmatter, err := checkFrontmatter(filePath)
if err != nil {
fmt.Println("Error checking frontmatter:", err)
return
}
// If the file doesn't have frontmatter, add it
if !hasFrontmatter {
err = addFrontmatter(filePath, fileName)
if err != nil {
fmt.Println("Error adding frontmatter:", err)
return
}
}
err = copyFile(filePath, newFilePath)
if err != nil {
fmt.Println("Error copying file:", err)
return
}
// Move images to the gallery directory
err = moveImagesToGallery(filePath, newDir, directory)
if err != nil {
fmt.Println("Error moving images to the gallery:", err)
return
}
}
// Function to check if the file already has frontmatter
func checkFrontmatter(filePath string) (bool, error) {
file, err := os.Open(filePath)
if err != nil {
return false, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if line == "---" {
return true, nil
}
}
return false, nil
}
// Function to add frontmatter to the file
func addFrontmatter(filePath, title string) error {
// Read the content of the original file
content, err := ioutil.ReadFile(filePath)
if err != nil {
return err
}
// Create a new temporary file with the frontmatter added
tempFilePath := filePath + ".tmp"
frontmatter := fmt.Sprintf("---\ntitle: %s\ndate: %s\ndraft: false\ntags: micropost\n---\n\n", title, time.Now().Format("2006-01-02 15:04:05 -0700"))
err = ioutil.WriteFile(tempFilePath, []byte(frontmatter+string(content)), os.ModePerm)
if err != nil {
return err
}
// Replace the original file with the temporary file
fmt.Println("Rename:", tempFilePath, filePath)
err = os.Rename(tempFilePath, filePath)
if err != nil {
return err
}
return nil
}
// Function to check if the file contains the "build: true" tag in the frontmatter
func hasBuildTag(filePath string) (bool, error) {
// Open the file
file, err := os.Open(filePath)
if err != nil {
return false, err
}
defer file.Close()
// Read the file line by line
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
// Check for the start of the frontmatter
if line == "---" {
var frontmatter []string
// Read the frontmatter
for scanner.Scan() {
line := scanner.Text()
if line == "---" {
break
}
frontmatter = append(frontmatter, line)
}
// Check if the frontmatter contains the "build: true" tag
for _, tag := range frontmatter {
if strings.Contains(tag, "build: true") {
return true, nil
}
}
break // We're done checking frontmatter, so break out of the loop
}
}
return false, nil
}
// Function to execute the build action specified in the .ini file
func executeBuildAction(action string) error {
// Split the action into command and arguments
parts := strings.Fields(action)
cmd := parts[0]
args := parts[1:]
// Execute the command
out, err := exec.Command(cmd, args...).Output()
if err != nil {
return err
}
// Print the command output
fmt.Println(string(out))
return nil
}
func copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
newFile, err := os.Create(dst)
if err != nil {
return err
}
defer newFile.Close()
_, err = io.Copy(newFile, sourceFile)
if err != nil {
return err
}
return nil
}
func removeBuildTag(filePath string) error {
// Read file content
content, err := ioutil.ReadFile(filePath)
if err != nil {
return err
}
contentStr := string(content)
// Find frontmatter
start := strings.Index(contentStr, "---")
end := strings.Index(contentStr[start+3:], "---")
// Verify if frontmatter exists
if start != -1 && end != -1 {
frontmatter := contentStr[start : start+3+end+3]
// Look for "build: true" in the frontmatter
buildTagIndex := strings.Index(frontmatter, "build: true")
if buildTagIndex != -1 {
// Delete "build: true" from frontmatter
frontmatter = strings.ReplaceAll(frontmatter, "build: true\n", "")
contentStr = strings.Replace(contentStr, string(content[start:start+3+end+3]), frontmatter, 1)
err := ioutil.WriteFile(filePath, []byte(contentStr), 0644)
if err != nil {
return err
}
}
}
return nil
}
func moveImagesToGallery(filePath, destDir, directory string) error {
// Routes
destFilePath := filepath.Join(destDir, "index.md")
slugDir := slug.Make(strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)))
galleryDir := filepath.Join(destDir, "gallery")
// Read file content
content, err := os.ReadFile(filePath)
if err != nil {
return err
}
// Regexp to find the markdown images
re := regexp.MustCompile(`!\[\[([^]]+)\]\]`)
matches := re.FindAllStringSubmatch(string(content), -1)
// Create gallery dir if it doesn't exist
if _, err := os.Stat(galleryDir); os.IsNotExist(err) {
err := os.Mkdir(galleryDir, os.ModePerm)
if err != nil {
return err
}
}
// Move every image found
for _, match := range matches {
imagePath := match[1]
imageName := filepath.Base(imagePath)
ext := filepath.Ext(imageName)
slugName := slug.Make(strings.TrimSuffix(imageName, ext))
imageSrc := filepath.Join(directory, imagePath)
imageDest := filepath.Join(galleryDir, slugName+ext)
// Open source file
srcFile, err := os.Open(imageSrc)
if err != nil {
fmt.Printf("Error opening source file %s: %s\n", imageSrc, err)
continue
}
defer srcFile.Close()
// Create destination file
destFile, err := os.Create(imageDest)
if err != nil {
fmt.Printf("Error creating destination file %s: %s\n", imageDest, err)
continue
}
defer destFile.Close()
// Copy source to destination
_, err = io.Copy(destFile, srcFile)
if err != nil {
fmt.Printf("Error copying file %s to %s: %s\n", imageSrc, imageDest, err)
continue
}
// Modify link text
newImageMarkdown := fmt.Sprintf("![%s](%s/gallery/%s)", strings.TrimSuffix(slugName, ext), slugDir, slugName+ext)
content = bytes.Replace(content, []byte(match[0]), []byte(newImageMarkdown), -1)
fmt.Printf("Image %s was copied to %s\n", imageSrc, imageDest)
}
// Replace [gallery] pattern
content = bytes.Replace(content, []byte("[gallery]"), []byte("{{< gallery match=\"gallery/*\" sortOrder=\"asc\" rowHeight=\"150\" margins=\"5\" thumbnailResizeOptions=\"600x600 q90 Lanczos\" previewType=\"blur\" embedPreview=\"true\" >}}"), -1)
// Write markdown file new content
err = os.WriteFile(destFilePath, content, os.ModePerm)
if err != nil {
fmt.Printf("Error writing file content to the file: %s: %s\n", filePath, err)
}
return nil
}