hoarder/hoarder.go

332 lines
8.1 KiB
Go

// Watch a torrent directory, poll rtorrent, and download completed torrents over SFTP.
package main
import (
"errors"
"flag"
"github.com/adampresley/sigint"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"time"
)
// Load information from a given config file config_path
func loadConfig(configPath string) (map[string]string, error) {
file, err := os.Open(configPath)
if err != nil {
log.Println("Failed to open configuration file " + configPath)
return nil, err
}
data, err := ioutil.ReadAll(file)
if err != nil {
log.Println("Failed to read configuration file " + configPath)
return nil, err
}
config := make(map[string]string)
lines := strings.Split(string(data), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
// Ignore comments
if len(line) <= 2 || line[:2] == "//" {
continue
}
// Ignore malformed lines
sepPosition := strings.Index(line, ": \"")
if sepPosition == -1 {
continue
}
config[line[:sepPosition]] = line[sepPosition+3 : len(line)-1]
}
return config, nil
}
// Checker routine to see if torrents are completed
func checker(config map[string]string, checkerChan <-chan map[string]string, com chan<- error) error {
for {
torrentInfo := <-checkerChan
log.Println("Started checking " + torrentInfo["torrent_path"])
torrent, err := NewTorrent(config["xml_user"], config["xml_pass"], config["xml_address"], torrentInfo["torrent_path"])
if err != nil {
if !os.IsNotExist(err) {
log.Println("Failed to initialize torrent for " + torrentInfo["torrent_path"] + ": " + err.Error())
}
continue
}
syncer, err := NewSync(config["threads"], config["ssh_user"], config["ssh_pass"], config["ssh_server"], config["ssh_port"])
defer syncer.Close()
if err != nil {
log.Println("Failed to create a new sync: " + err.Error())
com <- err
return err
}
completed, err := torrent.GetTorrentComplete()
if err != nil {
log.Println("Failed to see if " + torrent.path + " is completed: " + err.Error())
com <- err
return err
}
name, err := torrent.GetTorrentName()
if err != nil {
com <- err
return err
}
if completed {
log.Println(name + " is completed, starting download now")
remoteDownloadPath := filepath.Join(config["remote_download_dir"], name)
exists, err := syncer.Exists(remoteDownloadPath)
if err != nil {
log.Println("Failed to see if " + remoteDownloadPath + " exists: " + err.Error())
com <- err
return err
}
// file/dir to downlaod does not exist!
if !exists {
err = errors.New(remoteDownloadPath + " does not exist on remote server")
com <- err
return err
}
completedDestination := filepath.Join(torrentInfo["local_download_dir"], name)
_, err = os.Stat(completedDestination)
if err == nil {
err = errors.New(completedDestination + " already exists, not downloading")
continue
} else if !os.IsNotExist(err) {
log.Println("Failed to stat: " + completedDestination + ": " + err.Error())
com <- err
return err
}
err = syncer.GetPath(remoteDownloadPath, config["temp_download_dir"])
if err != nil {
log.Println("Failed to download " + remoteDownloadPath + ": " + err.Error())
com <- err
return err
}
log.Println("Successfully downloaded " + name)
tempDestination := filepath.Join(config["temp_download_dir"], name)
err = os.Rename(tempDestination, completedDestination)
if err != nil {
log.Println("Failed to move " + tempDestination + " to " + completedDestination + ": " + err.Error())
com <- err
return err
}
err = os.Remove(torrent.path)
if err != nil && !os.IsNotExist(err) {
log.Println("Failed to remove " + torrent.path + ": " + err.Error())
com <- err
return err
}
} else {
log.Println(name + " is not completed, waiting for it to finish")
}
syncer.Close()
}
com <- nil
return nil
}
// Scanner routine to see if there are new torrent_files
func scanner(config map[string]string, checkerChan chan<- map[string]string, com chan<- error) error {
watchDirs := map[string]string{config["local_torrent_dir"]: config["local_download_dir"]}
dirContents, err := ioutil.ReadDir(config["local_torrent_dir"])
if err != nil {
com <- err
return err
}
for _, file := range dirContents {
if file.IsDir() {
watchDirs[filepath.Join(config["local_torrent_dir"], file.Name())] = filepath.Join(config["local_download_dir"], file.Name())
}
}
uploaded := make(map[string]bool)
downloadingTorrentPath := ""
for {
for watchDir, downloadDir := range watchDirs {
torrentFiles, err := ioutil.ReadDir(watchDir)
if err != nil {
com <- err
return err
}
for _, torrentFile := range torrentFiles {
if torrentFile.IsDir() {
// skip because we don't do more than one level of watching
continue
}
torrentPath := filepath.Join(watchDir, torrentFile.Name())
if !uploaded[torrentPath] {
syncer, err := NewSync("1", config["ssh_user"], config["ssh_pass"], config["ssh_server"], config["ssh_port"])
if err != nil {
log.Println("Failed to create a new sync: " + err.Error())
syncer.Close()
continue
}
destinationTorrent := filepath.Join(config["remote_torrent_dir"], filepath.Base(torrentPath))
exists, err := syncer.Exists(destinationTorrent)
if err != nil {
log.Println("Failed to see if " + torrentPath + " already exists on the server: " + err.Error())
syncer.Close()
continue
}
if exists {
uploaded[torrentPath] = true
} else {
err = syncer.SendFiles(map[string]string{torrentPath: destinationTorrent})
if err == nil {
log.Println("Successfully uploaded " + torrentPath + " to " + destinationTorrent)
uploaded[torrentPath] = true
} else {
log.Println("Failed to upload " + torrentPath + " to " + destinationTorrent + ": " + err.Error())
}
syncer.Close()
continue
}
syncer.Close()
}
downloadInfo := map[string]string{
"torrent_path": torrentPath,
"local_download_dir": downloadDir,
}
// try to send the info to the checker goroutine (nonblocking)
select {
case checkerChan <- downloadInfo:
// don't keep track of completed downloads in the uploaded map
if downloadingTorrentPath != "" {
delete(uploaded, downloadingTorrentPath)
}
downloadingTorrentPath = torrentPath
break
default:
break
}
}
}
time.Sleep(time.Second * 30)
}
com <- nil
return nil
}
func die(exitCode int) {
log.Println("Quiting")
os.Exit(exitCode)
}
func main() {
sigint.ListenForSIGINT(func() {
die(1)
})
var configPath string
flag.StringVar(&configPath, "config", "", "Location of the config file")
flag.Parse()
if configPath == "" {
log.Println("Missing argument for configuration file path")
flag.PrintDefaults()
die(1)
}
log.Println("Reading configuration file")
config, err := loadConfig(configPath)
if err != nil {
log.Println(err)
die(1)
}
log.Println("Successfully read configuration file")
checkerChan := make(chan map[string]string, 50)
if err != nil {
log.Println(err)
die(1)
}
log.Println("Starting the scanner routine")
scannerCom := make(chan error)
go scanner(config, checkerChan, scannerCom)
log.Println("Starting the checker routine")
checkerCom := make(chan error)
go checker(config, checkerChan, checkerCom)
restartOnError := true
if config["restart_on_error"] != "" {
restartOnError = config["restart_on_error"] == "true"
}
for {
select {
case err := <-scannerCom:
if err != nil {
log.Println("Scanner failed: " + err.Error())
if restartOnError {
log.Println("Restarting scanner")
go scanner(config, checkerChan, scannerCom)
} else {
log.Println("Quiting due to scanner error")
die(1)
}
}
case err := <-checkerCom:
if err != nil {
log.Println("Checker failed: " + err.Error())
if restartOnError {
log.Println("Restarting checker")
go checker(config, checkerChan, checkerCom)
} else {
log.Println("Quiting due to checker error")
die(1)
}
}
default:
break
}
time.Sleep(time.Second * 5)
}
}