260 lines
7.2 KiB
Go
260 lines
7.2 KiB
Go
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
|
|
}
|