Untitled

 avatar
unknown
golang
15 days ago
6.5 kB
3
Indexable
package main

import (
	"flag"
	"io"
	"log"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/fsnotify/fsnotify"
	"github.com/studio-b12/gowebdav"
)

var (
	watcher       *fsnotify.Watcher
	dav           *gowebdav.Client
	localPath     string
	serverBase    string
	serverPath    string
	user          string
	password      string
	syncMutex     sync.Mutex
	periodicCheck *time.Ticker
)

func main() {
	// Parse command line flags
	flagLocalPath := flag.String("local_path", "", "Local directory path (env: WEBDAV_LOCAL_PATH)")
	flagURI := flag.String("uri", "", "WebDAV server URI (env: WEBDAV_URI)")
	flagUser := flag.String("user", "", "WebDAV username (env: WEBDAV_USER)")
	flagPassword := flag.String("password", "", "WebDAV password (env: WEBDAV_PASSWORD)")
	flag.Parse()

	// Get values from environment if flags are empty
	localPath = getEnvOrDefault(*flagLocalPath, "WEBDAV_LOCAL_PATH")
	uri := getEnvOrDefault(*flagURI, "WEBDAV_URI")
	user = getEnvOrDefault(*flagUser, "WEBDAV_USER")
	password = getEnvOrDefault(*flagPassword, "WEBDAV_PASSWORD")

	// Validate required parameters
	if localPath == "" || uri == "" || user == "" || password == "" {
		log.Fatal("Missing required parameters. Provide via flags or environment variables")
	}

	// Parse server URI
	parsedURI, err := url.Parse(uri)
	if err != nil {
		log.Fatalf("Invalid URI: %v", err)
	}
	serverBase = parsedURI.Scheme + "://" + parsedURI.Host
	serverPath = parsedURI.Path

	// Initialize WebDAV client
	dav = gowebdav.NewClient(serverBase, user, password)

	// Initial sync from server
	log.Println("Starting initial sync from server...")
	if err := syncFromServer(serverPath, localPath); err != nil {
		log.Fatalf("Initial sync failed: %v", err)
	}
	log.Println("Initial sync completed")

	// Initialize file watcher
	watcher, err = fsnotify.NewWatcher()
	if err != nil {
		log.Fatalf("Watcher init failed: %v", err)
	}
	defer watcher.Close()

	// Add watchers for all directories
	if err := filepath.Walk(localPath, watchDir); err != nil {
		log.Fatalf("Failed to initialize watchers: %v", err)
	}

	// Start periodic server checks
	periodicCheck = time.NewTicker(5 * time.Minute)
	defer periodicCheck.Stop()
	go periodicSync()

	// Start watching for changes
	for {
		select {
		case event, ok := <-watcher.Events:
			if !ok {
				return
			}
			processEvent(event)
		case err, ok := <-watcher.Errors:
			if !ok {
				return
			}
			log.Printf("Watcher error: %v", err)
		}
	}
}

func getEnvOrDefault(flagValue, envVar string) string {
	if flagValue != "" {
		return flagValue
	}
	return os.Getenv(envVar)
}

func watchDir(path string, fi os.FileInfo, err error) error {
	if err != nil {
		return err
	}
	if fi.Mode().IsDir() {
		return watcher.Add(path)
	}
	return nil
}

func syncFromServer(remotePath, localPath string) error {
	items, err := dav.ReadDir(remotePath)
	if err != nil {
		return err
	}

	for _, item := range items {
		remoteFull := filepath.Join(remotePath, item.Name())
		localFull := filepath.Join(localPath, item.Name())

		if item.IsDir() {
			if err := os.MkdirAll(localFull, 0755); err != nil {
				return err
			}
			if err := syncFromServer(remoteFull, localFull); err != nil {
				return err
			}
		} else {
			if needsDownload(localFull, item.Size()) {
				if err := downloadFile(remoteFull, localFull); err != nil {
					return err
				}
			}
		}
	}
	return nil
}

func needsDownload(localPath string, remoteSize int64) bool {
	info, err := os.Stat(localPath)
	if os.IsNotExist(err) {
		return true
	}
	if err != nil || info.Size() != remoteSize {
		return true
	}
	return false
}

func downloadFile(remotePath, localPath string) error {
	reader, err := dav.ReadStream(remotePath)
	if err != nil {
		return err
	}
	defer reader.Close()

	file, err := os.Create(localPath)
	if err != nil {
		return err
	}
	defer file.Close()

	_, err = io.Copy(file, reader)
	return err
}

func processEvent(event fsnotify.Event) {
	syncMutex.Lock()
	defer syncMutex.Unlock()

	// Ignore temporary files and directories
	if strings.HasSuffix(event.Name, "~") || event.Op == fsnotify.Chmod {
		return
	}

	relPath, err := filepath.Rel(localPath, event.Name)
	if err != nil {
		log.Printf("Error getting relative path: %v", err)
		return
	}
	webdavPath := filepath.Join(serverPath, relPath)

	switch {
	case event.Op&fsnotify.Write == fsnotify.Write:
		handleWrite(event.Name, webdavPath)
	case event.Op&fsnotify.Create == fsnotify.Create:
		handleCreate(event.Name, webdavPath)
	case event.Op&fsnotify.Remove == fsnotify.Remove:
		handleRemove(webdavPath)
	case event.Op&fsnotify.Rename == fsnotify.Rename:
		handleRemove(webdavPath)
	}
}

func handleWrite(localPath, webdavPath string) {
	if isSameSize(localPath, webdavPath) {
		return
	}

	file, err := os.Open(localPath)
	if err != nil {
		log.Printf("Error opening file: %v", err)
		return
	}
	defer file.Close()

	if err := dav.WriteStream(webdavPath, file, 0644); err != nil {
		log.Printf("Upload failed: %v", err)
	} else {
		log.Printf("Uploaded: %s", webdavPath)
	}
}

func handleCreate(localPath, webdavPath string) {
	if isDir(localPath) {
		if err := dav.MkdirAll(webdavPath, 0755); err != nil {
			log.Printf("Directory creation failed: %v", err)
		} else {
			watcher.Add(localPath)
			log.Printf("Created directory: %s", webdavPath)
		}
	} else {
		handleWrite(localPath, webdavPath)
	}
}

func handleRemove(webdavPath string) {
	if err := dav.Remove(webdavPath); err != nil {
		log.Printf("Deletion failed: %v", err)
	} else {
		log.Printf("Deleted: %s", webdavPath)
	}
}

func isDir(path string) bool {
	info, err := os.Stat(path)
	return err == nil && info.IsDir()
}

func isSameSize(localPath, webdavPath string) bool {
	localInfo, err := os.Stat(localPath)
	if err != nil {
		return false
	}

	remoteInfo, err := dav.Stat(webdavPath)
	if err != nil {
		return false
	}

	return localInfo.Size() == remoteInfo.Size()
}

func periodicSync() {
	for range periodicCheck.C {
		syncMutex.Lock()
		log.Println("Starting periodic server sync...")
		if err := syncFromServer(serverPath, localPath); err != nil {
			log.Printf("Periodic sync failed: %v", err)
		} else {
			log.Println("Periodic sync completed")
		}
		syncMutex.Unlock()
	}
}
Leave a Comment