@@ -0,0 +1,259 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
CONFIG_PATH_VAR = "NEXTCLOUD_CONFIG_PATH"
|
||||
NEXTCLOUD_USER_VAR = "NEXTCLOUD_USER"
|
||||
NEXTCLOUD_PASSWORD_VAR = "NEXTCLOUD_PASSWORD"
|
||||
)
|
||||
|
||||
var (
|
||||
config Config
|
||||
logger *slog.Logger
|
||||
)
|
||||
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
|
||||
jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelDebug,
|
||||
AddSource: true,
|
||||
})
|
||||
logger = slog.New(jsonHandler)
|
||||
slog.SetDefault(logger)
|
||||
|
||||
configPath, exists := os.LookupEnv(CONFIG_PATH_VAR)
|
||||
if !exists {
|
||||
configPath = "./config.yaml"
|
||||
}
|
||||
logger.InfoContext(ctx,
|
||||
"Using config path",
|
||||
slog.String("configPath", configPath),
|
||||
)
|
||||
|
||||
file, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "Error reading config file", slog.Any("error", err))
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(file, &config)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "Error parsing config file", slog.Any("error", err))
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case os.Getenv(NEXTCLOUD_USER_VAR) == "":
|
||||
logger.ErrorContext(ctx, "NEXTCLOUD_USER environment variable is not set")
|
||||
os.Exit(1)
|
||||
return
|
||||
case os.Getenv(NEXTCLOUD_PASSWORD_VAR) == "":
|
||||
logger.ErrorContext(ctx, "NEXTCLOUD_PASSWORD environment variable is not set")
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
if err := config.Validate(); err != nil {
|
||||
logger.ErrorContext(ctx, "Invalid config", slog.Any("error", err))
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
client := NewClient(config.NextcloudURL, os.Getenv(NEXTCLOUD_USER_VAR), os.Getenv(NEXTCLOUD_PASSWORD_VAR))
|
||||
|
||||
for _, job := range config.Jobs {
|
||||
err := processJob(ctx, client, job)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "Failed to process job",
|
||||
slog.String("sourceDirectory", job.SourceDirectory),
|
||||
slog.String("destinationDirectory", job.DestinationDirectory),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
continue
|
||||
} else {
|
||||
logger.DebugContext(ctx, "Finished processing job",
|
||||
slog.String("sourceDirectory", job.SourceDirectory),
|
||||
slog.String("destinationDirectory", job.DestinationDirectory),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func processJob(ctx context.Context, client *Client, job Job) error {
|
||||
// 1. Check that we actually have files to upload...
|
||||
files, err := os.ReadDir(job.SourceDirectory)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "Failed to read source directory",
|
||||
slog.String("sourceDirectory", job.SourceDirectory),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
logger.InfoContext(ctx, "No files found in source directory, skipping job",
|
||||
slog.String("sourceDirectory", job.SourceDirectory),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 2. Ensure the destination exists in Nextcloud
|
||||
if err := client.CreateDestDirectory(ctx, job.DestinationDirectory); err != nil {
|
||||
logger.ErrorContext(ctx, "Failed to create destination directory in Nextcloud",
|
||||
slog.String("destinationDirectory", job.DestinationDirectory),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. Upload files
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
fileName := filepath.Base(file.Name())
|
||||
filePath := fmt.Sprintf("%s/%s", job.SourceDirectory, fileName)
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "Error opening file",
|
||||
slog.String("sourceDirectory", job.SourceDirectory),
|
||||
slog.String("fileName", fileName),
|
||||
slog.String("filePath", filePath),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
logger.DebugContext(ctx, "Uploading file",
|
||||
slog.String("sourceDirectory", job.SourceDirectory),
|
||||
slog.String("destinationDirectory", job.DestinationDirectory),
|
||||
slog.String("fileName", fileName),
|
||||
slog.String("filePath", filePath),
|
||||
)
|
||||
if err := client.UploadFile(ctx, job.DestinationDirectory, file); err != nil {
|
||||
logger.ErrorContext(ctx, "Failed to upload file",
|
||||
slog.String("sourceDirectory", job.SourceDirectory),
|
||||
slog.String("destinationDirectory", job.DestinationDirectory),
|
||||
slog.String("fileName", fileName),
|
||||
slog.String("filePath", filePath),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Remove any files older than the max age specified
|
||||
if job.MaxAge != nil {
|
||||
duration, _ := time.ParseDuration(*job.MaxAge)
|
||||
threshold := time.Now().Add(-duration)
|
||||
logger.InfoContext(ctx, "Removing files older than max age",
|
||||
slog.String("destinationDirectory", job.DestinationDirectory),
|
||||
slog.String("maxAge", *job.MaxAge),
|
||||
slog.Time("dateThreshold", threshold),
|
||||
)
|
||||
|
||||
contents, err := client.ListDirectoryContents(ctx, job.DestinationDirectory)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "Failed to list directory contents",
|
||||
slog.String("sourceDirectory", job.SourceDirectory),
|
||||
slog.String("destinationDirectory", job.DestinationDirectory),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range contents {
|
||||
if file.LastModified != nil {
|
||||
if file.LastModified.Before(threshold) {
|
||||
logger.InfoContext(ctx, "Removing old file",
|
||||
slog.String("destinationDirectory", job.DestinationDirectory),
|
||||
slog.String("fileName", file.FileName),
|
||||
slog.Time("lastModified", *file.LastModified),
|
||||
)
|
||||
if err := client.DeleteFile(ctx, job.DestinationDirectory, file.FileName); err != nil {
|
||||
logger.ErrorContext(ctx, "Failed to delete file",
|
||||
slog.String("destinationDirectory", job.DestinationDirectory),
|
||||
slog.String("fileName", file.FileName),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Ensure only newest X files are kept
|
||||
if job.MaxItems != nil {
|
||||
logger.InfoContext(ctx, "Removing excess files",
|
||||
slog.String("destinationDirectory", job.DestinationDirectory),
|
||||
slog.Int("maxItem", *job.MaxItems),
|
||||
)
|
||||
|
||||
contents, err := client.ListDirectoryContents(ctx, job.DestinationDirectory)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "Failed to list directory contents",
|
||||
slog.String("sourceDirectory", job.SourceDirectory),
|
||||
slog.String("destinationDirectory", job.DestinationDirectory),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
sort.SliceStable(contents, func(i, j int) bool {
|
||||
if contents[i].LastModified == nil && contents[j].LastModified == nil {
|
||||
return false
|
||||
}
|
||||
if contents[i].LastModified == nil {
|
||||
return false // nil goes to the end
|
||||
}
|
||||
if contents[j].LastModified == nil {
|
||||
return true // nil goes to the end
|
||||
}
|
||||
return contents[i].LastModified.After(*contents[j].LastModified) // flipped: After instead of Before
|
||||
})
|
||||
|
||||
if len(contents) > *job.MaxItems {
|
||||
for _, file := range contents[*job.MaxItems:] {
|
||||
logger.InfoContext(ctx, "Removing old files until max files met",
|
||||
slog.String("destinationDirectory", job.DestinationDirectory),
|
||||
slog.String("fileName", file.FileName),
|
||||
slog.Time("lastModified", *file.LastModified),
|
||||
slog.String("maxItems", *job.MaxAge),
|
||||
)
|
||||
if err := client.DeleteFile(ctx, job.DestinationDirectory, file.FileName); err != nil {
|
||||
logger.ErrorContext(ctx, "Failed to delete file",
|
||||
slog.String("destinationDirectory", job.DestinationDirectory),
|
||||
slog.String("fileName", file.FileName),
|
||||
slog.String("maxItems", *job.MaxAge),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user