diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..d714246
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,5 @@
+TRANSMISSION_ADDR=http://localhost:9091
+TRANSMISSION_PASSWORD=
+TRANSMISSION_USERNAME=
+WEB_ADDR=:19091
+WEB_PATH=/metrics
diff --git a/.gitignore b/.gitignore
index 8390d84..b07b78b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+/.env
/transmission-exporter
*.o
diff --git a/README.md b/README.md
index b5e9ae2..7fef856 100644
--- a/README.md
+++ b/README.md
@@ -19,12 +19,6 @@ ENV Variable | Description
| TRANSMISSION_USERNAME | Transmission username, no default |
| TRANSMISSION_PASSWORD | Transmission password, no default |
-### Build
-
- make
-
-For development we encourage you to use `make install` instead, it's faster.
-
### Docker
docker pull metalmatze/transmission-exporter
@@ -49,6 +43,14 @@ Example `docker-compose.yml` with Transmission also running in docker.
environment:
TRANSMISSION_ADDR: http://transmission:9091
+### Development
+
+ make
+
+For development we encourage you to use `make install` instead, it's faster.
+
+Now simply copy the `.env.example` to `.env`, like `cp .env.example .env` and set your preferences.
+Now you're good to go.
### Original authors of the Transmission package
Tobias Blom (https://github.com/tubbebubbe/transmission)
diff --git a/cmd/transmission-exporter/main.go b/cmd/transmission-exporter/main.go
index 8543cbd..7f159f0 100644
--- a/cmd/transmission-exporter/main.go
+++ b/cmd/transmission-exporter/main.go
@@ -5,22 +5,28 @@ import (
"net/http"
arg "github.com/alexflint/go-arg"
+ "github.com/joho/godotenv"
transmission "github.com/metalmatze/transmission-exporter"
"github.com/prometheus/client_golang/prometheus"
)
// Config gets its content from env and passes it on to different packages
type Config struct {
- WebPath string `arg:"env:WEB_PATH"`
- WebAddr string `arg:"env:WEB_ADDR"`
TransmissionAddr string `arg:"env:TRANSMISSION_ADDR"`
- TransmissionUsername string `arg:"env:TRANSMISSION_USERNAME"`
TransmissionPassword string `arg:"env:TRANSMISSION_PASSWORD"`
+ TransmissionUsername string `arg:"env:TRANSMISSION_USERNAME"`
+ WebAddr string `arg:"env:WEB_ADDR"`
+ WebPath string `arg:"env:WEB_PATH"`
}
func main() {
log.Println("starting transmission-exporter")
+ err := godotenv.Load()
+ if err != nil {
+ log.Println("no .env present")
+ }
+
c := Config{
WebPath: "/metrics",
WebAddr: ":19091",
@@ -40,8 +46,11 @@ func main() {
client := transmission.New(c.TransmissionAddr, user)
prometheus.MustRegister(NewTorrentCollector(client))
+ prometheus.MustRegister(NewSessionCollector(client))
+ prometheus.MustRegister(NewSessionStatsCollector(client))
http.Handle(c.WebPath, prometheus.Handler())
+
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`
Node Exporter
@@ -54,3 +63,10 @@ func main() {
log.Fatal(http.ListenAndServe(c.WebAddr, nil))
}
+
+func boolToString(true bool) string {
+ if true {
+ return "1"
+ }
+ return "0"
+}
diff --git a/cmd/transmission-exporter/session_collector.go b/cmd/transmission-exporter/session_collector.go
new file mode 100644
index 0000000..8fa7478
--- /dev/null
+++ b/cmd/transmission-exporter/session_collector.go
@@ -0,0 +1,200 @@
+package main
+
+import (
+ "log"
+
+ "github.com/metalmatze/transmission-exporter"
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+// SessionCollector exposes session metrics
+type SessionCollector struct {
+ client *transmission.Client
+
+ AltSpeedDown *prometheus.Desc
+ AltSpeedUp *prometheus.Desc
+ CacheSize *prometheus.Desc
+ FreeSpace *prometheus.Desc
+ QueueDown *prometheus.Desc
+ QueueUp *prometheus.Desc
+ PeerLimitGlobal *prometheus.Desc
+ PeerLimitTorrent *prometheus.Desc
+ SeedRatioLimit *prometheus.Desc
+ SpeedLimitDown *prometheus.Desc
+ SpeedLimitUp *prometheus.Desc
+ Version *prometheus.Desc
+}
+
+// NewSessionCollector takes a transmission.Client and returns a SessionCollector
+func NewSessionCollector(client *transmission.Client) *SessionCollector {
+ return &SessionCollector{
+ client: client,
+
+ AltSpeedDown: prometheus.NewDesc(
+ namespace+"alt_speed_down",
+ "Alternative max global download speed",
+ []string{"enabled"},
+ nil,
+ ),
+ AltSpeedUp: prometheus.NewDesc(
+ namespace+"alt_speed_up",
+ "Alternative max global upload speed",
+ []string{"enabled"},
+ nil,
+ ),
+ CacheSize: prometheus.NewDesc(
+ namespace+"cache_size_bytes",
+ "Maximum size of the disk cache",
+ nil,
+ nil,
+ ),
+ FreeSpace: prometheus.NewDesc(
+ namespace+"free_space",
+ "Free space left on device to download to",
+ []string{"download_dir", "incomplete_dir"},
+ nil,
+ ),
+ QueueDown: prometheus.NewDesc(
+ namespace+"queue_down",
+ "Max number of torrents to download at once",
+ []string{"enabled"},
+ nil,
+ ),
+ QueueUp: prometheus.NewDesc(
+ namespace+"queue_up",
+ "Max number of torrents to upload at once",
+ []string{"enabled"},
+ nil,
+ ),
+ PeerLimitGlobal: prometheus.NewDesc(
+ namespace+"global_peer_limit",
+ "Maximum global number of peers",
+ nil,
+ nil,
+ ),
+ PeerLimitTorrent: prometheus.NewDesc(
+ namespace+"torrent_peer_limit",
+ "Maximum number of peers for a single torrent",
+ nil,
+ nil,
+ ),
+ SeedRatioLimit: prometheus.NewDesc(
+ namespace+"seed_ratio_limit",
+ "The default seed ratio for torrents to use",
+ []string{"enabled"},
+ nil,
+ ),
+ SpeedLimitDown: prometheus.NewDesc(
+ namespace+"speed_limit_down_bytes",
+ "Max global download speed",
+ []string{"enabled"},
+ nil,
+ ),
+ SpeedLimitUp: prometheus.NewDesc(
+ namespace+"speed_limit_up_bytes",
+ "Max global upload speed",
+ []string{"enabled"},
+ nil,
+ ),
+ Version: prometheus.NewDesc(
+ namespace+"version",
+ "Transmission version as label",
+ []string{"version"},
+ nil,
+ ),
+ }
+}
+
+// Describe implements the prometheus.Collector interface
+func (sc *SessionCollector) Describe(ch chan<- *prometheus.Desc) {
+ ch <- sc.AltSpeedDown
+ ch <- sc.AltSpeedUp
+ ch <- sc.CacheSize
+ ch <- sc.FreeSpace
+ ch <- sc.QueueDown
+ ch <- sc.QueueUp
+ ch <- sc.PeerLimitGlobal
+ ch <- sc.PeerLimitTorrent
+ ch <- sc.SeedRatioLimit
+ ch <- sc.SpeedLimitDown
+ ch <- sc.SpeedLimitUp
+ ch <- sc.Version
+}
+
+// Collect implements the prometheus.Collector interface
+func (sc *SessionCollector) Collect(ch chan<- prometheus.Metric) {
+ session, err := sc.client.GetSession()
+ if err != nil {
+ log.Printf("failed to get session: %v", err)
+ }
+
+ ch <- prometheus.MustNewConstMetric(
+ sc.AltSpeedDown,
+ prometheus.GaugeValue,
+ float64(session.AltSpeedDown),
+ boolToString(session.AltSpeedEnabled),
+ )
+ ch <- prometheus.MustNewConstMetric(
+ sc.AltSpeedUp,
+ prometheus.GaugeValue,
+ float64(session.AltSpeedUp),
+ boolToString(session.AltSpeedEnabled),
+ )
+ ch <- prometheus.MustNewConstMetric(
+ sc.CacheSize,
+ prometheus.GaugeValue,
+ float64(session.CacheSizeMB*1024*1024),
+ )
+ ch <- prometheus.MustNewConstMetric(
+ sc.FreeSpace,
+ prometheus.GaugeValue,
+ float64(session.DownloadDirFreeSpace),
+ session.DownloadDir, session.IncompleteDir,
+ )
+ ch <- prometheus.MustNewConstMetric(
+ sc.QueueDown,
+ prometheus.GaugeValue,
+ float64(session.DownloadQueueSize),
+ boolToString(session.DownloadQueueEnabled),
+ )
+ ch <- prometheus.MustNewConstMetric(
+ sc.QueueUp,
+ prometheus.GaugeValue,
+ float64(session.SeedQueueSize),
+ boolToString(session.SeedQueueEnabled),
+ )
+ ch <- prometheus.MustNewConstMetric(
+ sc.PeerLimitGlobal,
+ prometheus.GaugeValue,
+ float64(session.PeerLimitGlobal),
+ )
+ ch <- prometheus.MustNewConstMetric(
+ sc.PeerLimitTorrent,
+ prometheus.GaugeValue,
+ float64(session.PeerLimitPerTorrent),
+ )
+ ch <- prometheus.MustNewConstMetric(
+ sc.SeedRatioLimit,
+ prometheus.GaugeValue,
+ float64(session.SeedRatioLimit),
+ boolToString(session.SeedRatioLimited),
+ )
+ ch <- prometheus.MustNewConstMetric(
+ sc.SpeedLimitDown,
+ prometheus.GaugeValue,
+ float64(session.SpeedLimitDown),
+ boolToString(session.SpeedLimitDownEnabled),
+ )
+ ch <- prometheus.MustNewConstMetric(
+ sc.SpeedLimitUp,
+ prometheus.GaugeValue,
+ float64(session.SpeedLimitUp),
+ boolToString(session.SpeedLimitUpEnabled),
+ )
+ ch <- prometheus.MustNewConstMetric(
+ sc.Version,
+ prometheus.GaugeValue,
+ float64(1),
+ session.Version,
+ )
+}
diff --git a/cmd/transmission-exporter/session_stats_collector.go b/cmd/transmission-exporter/session_stats_collector.go
new file mode 100644
index 0000000..4bcba88
--- /dev/null
+++ b/cmd/transmission-exporter/session_stats_collector.go
@@ -0,0 +1,185 @@
+package main
+
+import (
+ "log"
+ "time"
+
+ "github.com/metalmatze/transmission-exporter"
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+// SessionStatsCollector exposes SessionStats as metrics
+type SessionStatsCollector struct {
+ client *transmission.Client
+
+ DownloadSpeed *prometheus.Desc
+ UploadSpeed *prometheus.Desc
+ TorrentsTotal *prometheus.Desc
+ TorrentsActive *prometheus.Desc
+ TorrentsPaused *prometheus.Desc
+
+ Downloaded *prometheus.Desc
+ Uploaded *prometheus.Desc
+ FilesAdded *prometheus.Desc
+ ActiveTime *prometheus.Desc
+ SessionCount *prometheus.Desc
+}
+
+// NewSessionStatsCollector takes a transmission.Client and returns a SessionStatsCollector
+func NewSessionStatsCollector(client *transmission.Client) *SessionStatsCollector {
+ const collectorNamespace = "session_stats_"
+
+ return &SessionStatsCollector{
+ client: client,
+
+ DownloadSpeed: prometheus.NewDesc(
+ namespace+collectorNamespace+"download_speed_bytes",
+ "Current download speed in bytes",
+ nil,
+ nil,
+ ),
+ UploadSpeed: prometheus.NewDesc(
+ namespace+collectorNamespace+"upload_speed_bytes",
+ "Current download speed in bytes",
+ nil,
+ nil,
+ ),
+ TorrentsTotal: prometheus.NewDesc(
+ namespace+collectorNamespace+"torrents_total",
+ "The total number of torrents",
+ nil,
+ nil,
+ ),
+ TorrentsActive: prometheus.NewDesc(
+ namespace+collectorNamespace+"torrents_active",
+ "The number of active torrents",
+ nil,
+ nil,
+ ),
+ TorrentsPaused: prometheus.NewDesc(
+ namespace+collectorNamespace+"torrents_paused",
+ "The number of paused torrents",
+ nil,
+ nil,
+ ),
+
+ Downloaded: prometheus.NewDesc(
+ namespace+collectorNamespace+"downloaded_bytes",
+ "The number of downloaded bytes",
+ []string{"type"},
+ nil,
+ ),
+ Uploaded: prometheus.NewDesc(
+ namespace+collectorNamespace+"uploaded_bytes",
+ "The number of uploaded bytes",
+ []string{"type"},
+ nil,
+ ),
+ FilesAdded: prometheus.NewDesc(
+ namespace+collectorNamespace+"files_added",
+ "The number of files added",
+ []string{"type"},
+ nil,
+ ),
+ ActiveTime: prometheus.NewDesc(
+ namespace+collectorNamespace+"active",
+ "The time transmission is active since",
+ []string{"type"},
+ nil,
+ ),
+ SessionCount: prometheus.NewDesc(
+ namespace+collectorNamespace+"sessions",
+ "Count of the times transmission started",
+ []string{"type"},
+ nil,
+ ),
+ }
+}
+
+// Describe implements the prometheus.Collector interface
+func (sc *SessionStatsCollector) Describe(ch chan<- *prometheus.Desc) {
+ ch <- sc.DownloadSpeed
+ ch <- sc.UploadSpeed
+ ch <- sc.TorrentsTotal
+ ch <- sc.TorrentsActive
+ ch <- sc.TorrentsPaused
+}
+
+// Collect implements the prometheus.Collector interface
+func (sc *SessionStatsCollector) Collect(ch chan<- prometheus.Metric) {
+ stats, err := sc.client.GetSessionStats()
+ if err != nil {
+ log.Printf("failed to get session stats: %v", err)
+ }
+
+ ch <- prometheus.MustNewConstMetric(
+ sc.DownloadSpeed,
+ prometheus.GaugeValue,
+ float64(stats.DownloadSpeed),
+ )
+ ch <- prometheus.MustNewConstMetric(
+ sc.UploadSpeed,
+ prometheus.GaugeValue,
+ float64(stats.UploadSpeed),
+ )
+ ch <- prometheus.MustNewConstMetric(
+ sc.TorrentsTotal,
+ prometheus.GaugeValue,
+ float64(stats.TorrentCount),
+ )
+ ch <- prometheus.MustNewConstMetric(
+ sc.TorrentsActive,
+ prometheus.GaugeValue,
+ float64(stats.ActiveTorrentCount),
+ )
+ ch <- prometheus.MustNewConstMetric(
+ sc.TorrentsPaused,
+ prometheus.GaugeValue,
+ float64(stats.PausedTorrentCount),
+ )
+
+ types := []string{"current", "cumulative"}
+ for _, t := range types {
+ var stateStats transmission.SessionStateStats
+ if t == types[0] {
+ stateStats = stats.CurrentStats
+ } else {
+ stateStats = stats.CumulativeStats
+ }
+
+ ch <- prometheus.MustNewConstMetric(
+ sc.Downloaded,
+ prometheus.GaugeValue,
+ float64(stateStats.DownloadedBytes),
+ t,
+ )
+ ch <- prometheus.MustNewConstMetric(
+ sc.Uploaded,
+ prometheus.GaugeValue,
+ float64(stateStats.UploadedBytes),
+ t,
+ )
+ ch <- prometheus.MustNewConstMetric(
+ sc.FilesAdded,
+ prometheus.GaugeValue,
+ float64(stateStats.FilesAdded),
+ t,
+ )
+
+ dur := time.Duration(stateStats.SecondsActive) * time.Second
+ timestamp := time.Now().Add(-1 * dur).Unix()
+
+ ch <- prometheus.MustNewConstMetric(
+ sc.ActiveTime,
+ prometheus.GaugeValue,
+ float64(timestamp),
+ t,
+ )
+ ch <- prometheus.MustNewConstMetric(
+ sc.SessionCount,
+ prometheus.GaugeValue,
+ float64(stateStats.SessionCount),
+ t,
+ )
+ }
+}
diff --git a/cmd/transmission-exporter/torrent_collector.go b/cmd/transmission-exporter/torrent_collector.go
index cb9822b..33d8561 100644
--- a/cmd/transmission-exporter/torrent_collector.go
+++ b/cmd/transmission-exporter/torrent_collector.go
@@ -1,83 +1,134 @@
package main
import (
+ "log"
"strconv"
transmission "github.com/metalmatze/transmission-exporter"
"github.com/prometheus/client_golang/prometheus"
)
+const (
+ namespace string = "transmission_"
+)
+
// TorrentCollector has a transmission.Client to create torrent metrics
type TorrentCollector struct {
- client *transmission.Client
+ client *transmission.Client
+
Status *prometheus.Desc
+ Added *prometheus.Desc
+ Files *prometheus.Desc
Finished *prometheus.Desc
Done *prometheus.Desc
- Added *prometheus.Desc
Ratio *prometheus.Desc
Download *prometheus.Desc
Upload *prometheus.Desc
+
+ // TrackerStats
+ Downloads *prometheus.Desc
+ Leechers *prometheus.Desc
+ Seeders *prometheus.Desc
}
// NewTorrentCollector creates a new torrent collector with the transmission.Client
func NewTorrentCollector(client *transmission.Client) *TorrentCollector {
+ const collectorNamespace = "torrent_"
+
return &TorrentCollector{
client: client,
Status: prometheus.NewDesc(
- "transmission_torrent_status",
+ namespace+collectorNamespace+"status",
"Status of a torrent",
[]string{"id", "name"},
nil,
),
+ Added: prometheus.NewDesc(
+ namespace+collectorNamespace+"added",
+ "The unixtime time a torrent was added",
+ []string{"id", "name"},
+ nil,
+ ),
+ Files: prometheus.NewDesc(
+ namespace+collectorNamespace+"files_total",
+ "The unixtime time a torrent was added",
+ []string{"id", "name"},
+ nil,
+ ),
Finished: prometheus.NewDesc(
- "transmission_torrent_finished",
+ namespace+collectorNamespace+"finished",
"Indicates if a torrent is finished (1) or not (0)",
[]string{"id", "name"},
nil,
),
Done: prometheus.NewDesc(
- "transmission_torrent_done",
+ namespace+collectorNamespace+"done",
"The percent of a torrent being done",
[]string{"id", "name"},
nil,
),
- Added: prometheus.NewDesc(
- "transmission_torrent_added",
- "The unixtime time a torrent was added",
- []string{"id", "name"},
- nil,
- ),
Ratio: prometheus.NewDesc(
- "transmission_torrent_ratio",
+ namespace+collectorNamespace+"ratio",
"The upload ratio of a torrent",
[]string{"id", "name"},
nil,
),
Download: prometheus.NewDesc(
- "transmission_torrent_download_bytes",
+ namespace+collectorNamespace+"download_bytes",
"The current download rate of a torrent in bytes",
[]string{"id", "name"},
nil,
),
Upload: prometheus.NewDesc(
- "transmission_torrent_upload_bytes",
+ namespace+collectorNamespace+"upload_bytes",
"The current upload rate of a torrent in bytes",
[]string{"id", "name"},
nil,
),
+
+ // TrackerStats
+ Downloads: prometheus.NewDesc(
+ namespace+collectorNamespace+"downloads_total",
+ "How often this torrent was downloaded",
+ []string{"id", "name", "tracker"},
+ nil,
+ ),
+ Leechers: prometheus.NewDesc(
+ namespace+collectorNamespace+"leechers",
+ "The number of peers downloading this torrent",
+ []string{"id", "name", "tracker"},
+ nil,
+ ),
+ Seeders: prometheus.NewDesc(
+ namespace+collectorNamespace+"seeders",
+ "The number of peers uploading this torrent",
+ []string{"id", "name", "tracker"},
+ nil,
+ ),
}
}
// Describe implements the prometheus.Collector interface
func (tc *TorrentCollector) Describe(ch chan<- *prometheus.Desc) {
+ ch <- tc.Status
+ ch <- tc.Added
+ ch <- tc.Files
+ ch <- tc.Finished
+ ch <- tc.Done
ch <- tc.Ratio
+ ch <- tc.Download
+ ch <- tc.Upload
+ ch <- tc.Downloads
+ ch <- tc.Leechers
+ ch <- tc.Seeders
}
// Collect implements the prometheus.Collector interface
func (tc *TorrentCollector) Collect(ch chan<- prometheus.Metric) {
torrents, err := tc.client.GetTorrents()
if err != nil {
+ log.Printf("failed to get torrents: %v", err)
return
}
@@ -97,21 +148,27 @@ func (tc *TorrentCollector) Collect(ch chan<- prometheus.Metric) {
id, t.Name,
)
ch <- prometheus.MustNewConstMetric(
- tc.Finished,
+ tc.Added,
prometheus.GaugeValue,
- finished,
+ float64(t.Added),
id, t.Name,
)
ch <- prometheus.MustNewConstMetric(
- tc.Done,
+ tc.Files,
prometheus.GaugeValue,
- t.PercentDone,
+ float64(len(t.Files)),
id, t.Name,
)
ch <- prometheus.MustNewConstMetric(
- tc.Added,
+ tc.Finished,
prometheus.GaugeValue,
- float64(t.Added),
+ finished,
+ id, t.Name,
+ )
+ ch <- prometheus.MustNewConstMetric(
+ tc.Done,
+ prometheus.GaugeValue,
+ t.PercentDone,
id, t.Name,
)
ch <- prometheus.MustNewConstMetric(
@@ -132,5 +189,28 @@ func (tc *TorrentCollector) Collect(ch chan<- prometheus.Metric) {
float64(t.RateUpload),
id, t.Name,
)
+
+ for _, tracker := range t.TrackerStats {
+ ch <- prometheus.MustNewConstMetric(
+ tc.Downloads,
+ prometheus.GaugeValue,
+ float64(tracker.DownloadCount),
+ id, t.Name, tracker.Host,
+ )
+
+ ch <- prometheus.MustNewConstMetric(
+ tc.Leechers,
+ prometheus.GaugeValue,
+ float64(tracker.LeecherCount),
+ id, t.Name, tracker.Host,
+ )
+
+ ch <- prometheus.MustNewConstMetric(
+ tc.Seeders,
+ prometheus.GaugeValue,
+ float64(tracker.SeederCount),
+ id, t.Name, tracker.Host,
+ )
+ }
}
}
diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml
index e0e90bb..f93914b 100644
--- a/examples/docker-compose.yml
+++ b/examples/docker-compose.yml
@@ -5,6 +5,7 @@ transmission:
- "127.0.0.1:9091:9091"
- "51413:51413"
- "51413:51413/udp"
+
transmission-exporter:
image: metalmatze/transmission-exporter
restart: always
@@ -14,3 +15,13 @@ transmission-exporter:
- "127.0.0.1:19091:19091"
environment:
TRANSMISSION_ADDR: http://transmission:9091
+
+prometheus:
+ image: quay.io/prometheus/prometheus
+ command: "-config.file=/etc/prometheus/prometheus.yml"
+ links:
+ - transmission-exporter
+ ports:
+ - "127.0.0.1:9090:9090"
+ volumes:
+ - "./prometheus:/etc/prometheus/"
diff --git a/examples/prometheus/prometheus.yml b/examples/prometheus/prometheus.yml
new file mode 100644
index 0000000..fec7444
--- /dev/null
+++ b/examples/prometheus/prometheus.yml
@@ -0,0 +1,9 @@
+global:
+ scrape_interval: 15s
+ evaluation_interval: 15s
+
+scrape_configs:
+- job_name: 'transmission-exporter'
+ static_configs:
+ - targets:
+ - 'transmission-exporter:19091'
diff --git a/rpc_command.go b/rpc_command.go
deleted file mode 100644
index 264a3d8..0000000
--- a/rpc_command.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package transmission
-
-type (
- // RPCCommand is the root command to interact with Transmission via RPC
- RPCCommand struct {
- Method string `json:"method,omitempty"`
- Arguments RPCArguments `json:"arguments,omitempty"`
- Result string `json:"result,omitempty"`
- }
- // RPCArguments specifies the RPCCommand in more detail
- RPCArguments struct {
- Fields []string `json:"fields,omitempty"`
- Torrents []Torrent `json:"torrents,omitempty"`
- Ids []int `json:"ids,omitempty"`
- DeleteData bool `json:"delete-local-data,omitempty"`
- DownloadDir string `json:"download-dir,omitempty"`
- MetaInfo string `json:"metainfo,omitempty"`
- Filename string `json:"filename,omitempty"`
- TorrentAdded RPCTorrentAdded `json:"torrent-added"`
- }
- // RPCTorrentAdded specifies the torrent to get added data from
- RPCTorrentAdded struct {
- HashString string `json:"hashString"`
- ID int `json:"id"`
- Name string `json:"name"`
- }
-)
diff --git a/session.go b/session.go
new file mode 100644
index 0000000..1a5af49
--- /dev/null
+++ b/session.go
@@ -0,0 +1,62 @@
+package transmission
+
+type (
+ // SessionCommand is the root command to interact with Transmission via RPC
+ SessionCommand struct {
+ Method string `json:"method,omitempty"`
+ Session Session `json:"arguments,omitempty"`
+ Result string `json:"result,omitempty"`
+ }
+
+ // Session information about the current transmission session
+ Session struct {
+ AltSpeedDown int `json:"alt-speed-down"`
+ AltSpeedEnabled bool `json:"alt-speed-enabled"`
+ //Alt_speed_time_begin int `json:"alt-speed-time-begin"`
+ //Alt_speed_time_day int `json:"alt-speed-time-day"`
+ //Alt_speed_time_enabled bool `json:"alt-speed-time-enabled"`
+ //Alt_speed_time_end int `json:"alt-speed-time-end"`
+ AltSpeedUp int `json:"alt-speed-up"`
+ //Blocklist_enabled bool `json:"blocklist-enabled"`
+ //Blocklist_size int `json:"blocklist-size"`
+ //Blocklist_url string `json:"blocklist-url"`
+ CacheSizeMB int `json:"cache-size-mb"`
+ //Config_dir string `json:"config-dir"`
+ //Dht_enabled bool `json:"dht-enabled"`
+ DownloadDir string `json:"download-dir"`
+ DownloadDirFreeSpace int `json:"download-dir-free-space"`
+ DownloadQueueEnabled bool `json:"download-queue-enabled"`
+ DownloadQueueSize int `json:"download-queue-size"`
+ //Encryption string `json:"encryption"`
+ //Idle_seeding_limit int `json:"idle-seeding-limit"`
+ //Idle_seeding_limit_enabled bool `json:"idle-seeding-limit-enabled"`
+ IncompleteDir string `json:"incomplete-dir"`
+ //Incomplete_dir_enabled bool `json:"incomplete-dir-enabled"`
+ //Lpd_enabled bool `json:"lpd-enabled"`
+ PeerLimitGlobal int `json:"peer-limit-global"`
+ PeerLimitPerTorrent int `json:"peer-limit-per-torrent"`
+ //Peer_port int `json:"peer-port"`
+ //Peer_port_random_on_start bool `json:"peer-port-random-on-start"`
+ //Pex_enabled bool `json:"pex-enabled"`
+ //Port_forwarding_enabled bool `json:"port-forwarding-enabled"`
+ //Queue_stalled_enabled bool `json:"queue-stalled-enabled"`
+ //Queue_stalled_minutes int `json:"queue-stalled-minutes"`
+ //Rename_partial_files bool `json:"rename-partial-files"`
+ //RPC_version int `json:"rpc-version"`
+ //RPC_version_minimum int `json:"rpc-version-minimum"`
+ //Script_torrent_done_enabled bool `json:"script-torrent-done-enabled"`
+ //Script_torrent_done_filename string `json:"script-torrent-done-filename"`
+ SeedQueueEnabled bool `json:"seed-queue-enabled"`
+ SeedQueueSize int `json:"seed-queue-size"`
+ SeedRatioLimit int `json:"seedRatioLimit"`
+ SeedRatioLimited bool `json:"seedRatioLimited"`
+ SpeedLimitDown int `json:"speed-limit-down"`
+ SpeedLimitDownEnabled bool `json:"speed-limit-down-enabled"`
+ SpeedLimitUp int `json:"speed-limit-up"`
+ SpeedLimitUpEnabled bool `json:"speed-limit-up-enabled"`
+ //Start_added_torrents bool `json:"start-added-torrents"`
+ //Trash_original_torrent_files bool `json:"trash-original-torrent-files"`
+ //Utp_enabled bool `json:"utp-enabled"`
+ Version string `json:"version"`
+ }
+)
diff --git a/session_stats.go b/session_stats.go
new file mode 100644
index 0000000..4b5f7a9
--- /dev/null
+++ b/session_stats.go
@@ -0,0 +1,28 @@
+package transmission
+
+type (
+ // SessionStatsCmd is the root command to interact with Transmission via RPC
+ SessionStatsCmd struct {
+ SessionStats `json:"arguments"`
+ Result string `json:"result"`
+ }
+
+ // SessionStats contains information about the current & cumulative session
+ SessionStats struct {
+ DownloadSpeed int `json:"downloadSpeed"`
+ UploadSpeed int `json:"uploadSpeed"`
+ ActiveTorrentCount int `json:"activeTorrentCount"`
+ PausedTorrentCount int `json:"pausedTorrentCount"`
+ TorrentCount int `json:"torrentCount"`
+ CumulativeStats SessionStateStats `json:"cumulative-stats"`
+ CurrentStats SessionStateStats `json:"current-stats"`
+ }
+ // SessionStateStats contains current or cumulative session stats
+ SessionStateStats struct {
+ DownloadedBytes int `json:"downloadedBytes"`
+ UploadedBytes int `json:"uploadedBytes"`
+ FilesAdded int `json:"filesAdded"`
+ SecondsActive int `json:"secondsActive"`
+ SessionCount int `json:"sessionCount"`
+ }
+)
diff --git a/torrent.go b/torrent.go
index 41e4f73..c3a05e0 100644
--- a/torrent.go
+++ b/torrent.go
@@ -1,24 +1,52 @@
package transmission
type (
+ // TorrentCommand is the root command to interact with Transmission via RPC
+ TorrentCommand struct {
+ Method string `json:"method,omitempty"`
+ Arguments TorrentArguments `json:"arguments,omitempty"`
+ Result string `json:"result,omitempty"`
+ }
+ // TorrentArguments specifies the TorrentCommand in more detail
+ TorrentArguments struct {
+ Fields []string `json:"fields,omitempty"`
+ Torrents []Torrent `json:"torrents,omitempty"`
+ Ids []int `json:"ids,omitempty"`
+ DeleteData bool `json:"delete-local-data,omitempty"`
+ DownloadDir string `json:"download-dir,omitempty"`
+ MetaInfo string `json:"metainfo,omitempty"`
+ Filename string `json:"filename,omitempty"`
+ TorrentAdded TorrentArgumentsAdded `json:"torrent-added"`
+ }
+ // TorrentArgumentsAdded specifies the torrent to get added data from
+ TorrentArgumentsAdded struct {
+ HashString string `json:"hashString"`
+ ID int `json:"id"`
+ Name string `json:"name"`
+ }
+
// Torrent represents a transmission torrent
Torrent struct {
- ID int `json:"id"`
- Name string `json:"name"`
- Status int `json:"status"`
- Added int `json:"addedDate"`
- LeftUntilDone int `json:"leftUntilDone"`
- Eta int `json:"eta"`
- UploadRatio float64 `json:"uploadRatio"`
- RateDownload int `json:"rateDownload"`
- RateUpload int `json:"rateUpload"`
- DownloadDir string `json:"downloadDir"`
- IsFinished bool `json:"isFinished"`
- PercentDone float64 `json:"percentDone"`
- SeedRatioMode int `json:"seedRatioMode"`
- HashString string `json:"hashString"`
- Error int `json:"error"`
- ErrorString string `json:"errorString"`
+ ID int `json:"id"`
+ Name string `json:"name"`
+ Status int `json:"status"`
+ Added int `json:"addedDate"`
+ LeftUntilDone int `json:"leftUntilDone"`
+ Eta int `json:"eta"`
+ UploadRatio float64 `json:"uploadRatio"`
+ RateDownload int `json:"rateDownload"`
+ RateUpload int `json:"rateUpload"`
+ DownloadDir string `json:"downloadDir"`
+ IsFinished bool `json:"isFinished"`
+ PercentDone float64 `json:"percentDone"`
+ SeedRatioMode int `json:"seedRatioMode"`
+ HashString string `json:"hashString"`
+ Error int `json:"error"`
+ ErrorString string `json:"errorString"`
+ Files []File `json:"files"`
+ FilesStats []FileStat `json:"fileStats"`
+ TrackerStats []TrackerStat `json:"trackerStats"`
+ Peers []Peer `json:"peers"`
}
// ByID implements the sort Interface to sort by ID
@@ -29,6 +57,70 @@ type (
ByDate []Torrent
// ByRatio implements the sort Interface to sort by Ratio
ByRatio []Torrent
+
+ // File is a file contained inside a torrent
+ File struct {
+ BytesCompleted int `json:"bytesCompleted"`
+ Length int `json:"length"`
+ Name string `json:"name"`
+ }
+
+ // FileStat describe a file's priority & if it's wanted
+ FileStat struct {
+ BytesCompleted int `json:"bytesCompleted"`
+ Priority int `json:"priority"`
+ Wanted bool `json:"wanted"`
+ }
+
+ // TrackerStat has stats about the torrent's tracker
+ TrackerStat struct {
+ Announce string `json:"announce"`
+ AnnounceState int `json:"announceState"`
+ DownloadCount int `json:"downloadCount"`
+ HasAnnounced bool `json:"hasAnnounced"`
+ HasScraped bool `json:"hasScraped"`
+ Host string `json:"host"`
+ ID int `json:"id"`
+ IsBackup bool `json:"isBackup"`
+ LastAnnouncePeerCount int `json:"lastAnnouncePeerCount"`
+ LastAnnounceResult string `json:"lastAnnounceResult"`
+ LastAnnounceStartTime int `json:"lastAnnounceStartTime"`
+ LastAnnounceSucceeded bool `json:"lastAnnounceSucceeded"`
+ LastAnnounceTime int `json:"lastAnnounceTime"`
+ LastAnnounceTimedOut bool `json:"lastAnnounceTimedOut"`
+ LastScrapeResult string `json:"lastScrapeResult"`
+ LastScrapeStartTime int `json:"lastScrapeStartTime"`
+ LastScrapeSucceeded bool `json:"lastScrapeSucceeded"`
+ LastScrapeTime int `json:"lastScrapeTime"`
+ LastScrapeTimedOut int `json:"lastScrapeTimedOut"`
+ LeecherCount int `json:"leecherCount"`
+ NextAnnounceTime int `json:"nextAnnounceTime"`
+ NextScrapeTime int `json:"nextScrapeTime"`
+ Scrape string `json:"scrape"`
+ ScrapeState int `json:"scrapeState"`
+ SeederCount int `json:"seederCount"`
+ Tier int `json:"tier"`
+ }
+
+ // Peer of a torrent
+ Peer struct {
+ Address string `json:"address"`
+ ClientIsChoked bool `json:"clientIsChoked"`
+ ClientIsInterested bool `json:"clientIsInterested"`
+ ClientName string `json:"clientName"`
+ FlagStr string `json:"flagStr"`
+ IsDownloadingFrom bool `json:"isDownloadingFrom"`
+ IsEncrypted bool `json:"isEncrypted"`
+ IsIncoming bool `json:"isIncoming"`
+ IsUTP bool `json:"isUTP"`
+ IsUploadingTo bool `json:"isUploadingTo"`
+ PeerIsChoked bool `json:"peerIsChoked"`
+ PeerIsInterested bool `json:"peerIsInterested"`
+ Port int `json:"port"`
+ Progress float64 `json:"progress"`
+ RateToClient int `json:"rateToClient"`
+ RateToPeer int `json:"rateToPeer"`
+ }
)
func (t ByID) Len() int { return len(t) }
diff --git a/transmission.go b/transmission.go
index ba40ac5..fddebdc 100644
--- a/transmission.go
+++ b/transmission.go
@@ -104,33 +104,11 @@ func (c *Client) authRequest(method string, body []byte) (*http.Request, error)
return req, nil
}
-// ExecuteCommand sends the RPCCommand to Transmission and get the response
-func (c *Client) ExecuteCommand(cmd *RPCCommand) (*RPCCommand, error) {
- var out RPCCommand
-
- body, err := json.Marshal(&cmd)
- if err != nil {
- return nil, err
- }
-
- output, err := c.post(body)
- if err != nil {
- return nil, err
- }
-
- err = json.Unmarshal(output, &out)
- if err != nil {
- return nil, err
- }
-
- return &out, nil
-}
-
// GetTorrents get a list of torrents
func (c *Client) GetTorrents() ([]Torrent, error) {
- cmd := &RPCCommand{
+ cmd := TorrentCommand{
Method: "torrent-get",
- Arguments: RPCArguments{
+ Arguments: TorrentArguments{
Fields: []string{
"id",
"name",
@@ -148,14 +126,69 @@ func (c *Client) GetTorrents() ([]Torrent, error) {
"seedRatioMode",
"error",
"errorString",
+ "files",
+ "fileStats",
+ "peers",
+ "trackers",
+ "trackerStats",
},
},
}
- out, err := c.ExecuteCommand(cmd)
+ req, err := json.Marshal(&cmd)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := c.post(req)
if err != nil {
return nil, err
}
+ var out TorrentCommand
+ if err := json.Unmarshal(resp, &out); err != nil {
+ return nil, err
+ }
+
return out.Arguments.Torrents, nil
}
+
+// GetSession gets the current session from transmission
+func (c *Client) GetSession() (*Session, error) {
+ req, err := json.Marshal(SessionCommand{Method: "session-get"})
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := c.post(req)
+ if err != nil {
+ return nil, err
+ }
+
+ var cmd SessionCommand
+ if err := json.Unmarshal(resp, &cmd); err != nil {
+ return nil, err
+ }
+
+ return &cmd.Session, nil
+}
+
+// GetSessionStats gets stats on the current & cumulative session
+func (c *Client) GetSessionStats() (*SessionStats, error) {
+ req, err := json.Marshal(SessionCommand{Method: "session-stats"})
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := c.post(req)
+ if err != nil {
+ return nil, err
+ }
+
+ var cmd SessionStatsCmd
+ if err := json.Unmarshal(resp, &cmd); err != nil {
+ return nil, err
+ }
+
+ return &cmd.SessionStats, nil
+}
diff --git a/vendor/github.com/joho/godotenv/LICENCE b/vendor/github.com/joho/godotenv/LICENCE
new file mode 100644
index 0000000..e7ddd51
--- /dev/null
+++ b/vendor/github.com/joho/godotenv/LICENCE
@@ -0,0 +1,23 @@
+Copyright (c) 2013 John Barton
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/vendor/github.com/joho/godotenv/README.md b/vendor/github.com/joho/godotenv/README.md
new file mode 100644
index 0000000..05c47e6
--- /dev/null
+++ b/vendor/github.com/joho/godotenv/README.md
@@ -0,0 +1,127 @@
+# GoDotEnv [![wercker status](https://app.wercker.com/status/507594c2ec7e60f19403a568dfea0f78 "wercker status")](https://app.wercker.com/project/bykey/507594c2ec7e60f19403a568dfea0f78)
+
+A Go (golang) port of the Ruby dotenv project (which loads env vars from a .env file)
+
+From the original Library:
+
+> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables.
+>
+> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped.
+
+It can be used as a library (for loading in env for your own daemons etc) or as a bin command.
+
+There is test coverage and CI for both linuxish and windows environments, but I make no guarantees about the bin version working on windows.
+
+## Installation
+
+As a library
+
+```shell
+go get github.com/joho/godotenv
+```
+
+or if you want to use it as a bin command
+```shell
+go get github.com/joho/godotenv/cmd/godotenv
+```
+
+## Usage
+
+Add your application configuration to your `.env` file in the root of your project:
+
+```shell
+S3_BUCKET=YOURS3BUCKET
+SECRET_KEY=YOURSECRETKEYGOESHERE
+```
+
+Then in your Go app you can do something like
+
+```go
+package main
+
+import (
+ "github.com/joho/godotenv"
+ "log"
+ "os"
+)
+
+func main() {
+ err := godotenv.Load()
+ if err != nil {
+ log.Fatal("Error loading .env file")
+ }
+
+ s3Bucket := os.Getenv("S3_BUCKET")
+ secretKey := os.Getenv("SECRET_KEY")
+
+ // now do something with s3 or whatever
+}
+```
+
+If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import
+
+```go
+import _ "github.com/joho/godotenv/autoload"
+```
+
+While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit
+
+```go
+_ = godotenv.Load("somerandomfile")
+_ = godotenv.Load("filenumberone.env", "filenumbertwo.env")
+```
+
+If you want to be really fancy with your env file you can do comments and exports (below is a valid env file)
+
+```shell
+# I am a comment and that is OK
+SOME_VAR=someval
+FOO=BAR # comments at line end are OK too
+export BAR=BAZ
+```
+
+Or finally you can do YAML(ish) style
+
+```yaml
+FOO: bar
+BAR: baz
+```
+
+as a final aside, if you don't want godotenv munging your env you can just get a map back instead
+
+```go
+var myEnv map[string]string
+myEnv, err := godotenv.Read()
+
+s3Bucket := myEnv["S3_BUCKET"]
+```
+
+### Command Mode
+
+Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH`
+
+```
+godotenv -f /some/path/to/.env some_command with some args
+```
+
+If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD`
+
+## Contributing
+
+Contributions are most welcome! The parser itself is pretty stupidly naive and I wouldn't be surprised if it breaks with edge cases.
+
+*code changes without tests will not be accepted*
+
+1. Fork it
+2. Create your feature branch (`git checkout -b my-new-feature`)
+3. Commit your changes (`git commit -am 'Added some feature'`)
+4. Push to the branch (`git push origin my-new-feature`)
+5. Create new Pull Request
+
+## CI
+
+Linux: [![wercker status](https://app.wercker.com/status/507594c2ec7e60f19403a568dfea0f78/m "wercker status")](https://app.wercker.com/project/bykey/507594c2ec7e60f19403a568dfea0f78) Windows: [![Build status](https://ci.appveyor.com/api/projects/status/9v40vnfvvgde64u4)](https://ci.appveyor.com/project/joho/godotenv)
+
+## Who?
+
+The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](http://whoisjohnbarton.com) based off the tests/fixtures in the original library.
diff --git a/vendor/github.com/joho/godotenv/godotenv.go b/vendor/github.com/joho/godotenv/godotenv.go
new file mode 100644
index 0000000..94b2676
--- /dev/null
+++ b/vendor/github.com/joho/godotenv/godotenv.go
@@ -0,0 +1,229 @@
+// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv)
+//
+// Examples/readme can be found on the github page at https://github.com/joho/godotenv
+//
+// The TL;DR is that you make a .env file that looks something like
+//
+// SOME_ENV_VAR=somevalue
+//
+// and then in your go code you can call
+//
+// godotenv.Load()
+//
+// and all the env vars declared in .env will be avaiable through os.Getenv("SOME_ENV_VAR")
+package godotenv
+
+import (
+ "bufio"
+ "errors"
+ "os"
+ "os/exec"
+ "strings"
+)
+
+// Load will read your env file(s) and load them into ENV for this process.
+//
+// Call this function as close as possible to the start of your program (ideally in main)
+//
+// If you call Load without any args it will default to loading .env in the current path
+//
+// You can otherwise tell it which files to load (there can be more than one) like
+//
+// godotenv.Load("fileone", "filetwo")
+//
+// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults
+func Load(filenames ...string) (err error) {
+ filenames = filenamesOrDefault(filenames)
+
+ for _, filename := range filenames {
+ err = loadFile(filename, false)
+ if err != nil {
+ return // return early on a spazout
+ }
+ }
+ return
+}
+
+// Overload will read your env file(s) and load them into ENV for this process.
+//
+// Call this function as close as possible to the start of your program (ideally in main)
+//
+// If you call Overload without any args it will default to loading .env in the current path
+//
+// You can otherwise tell it which files to load (there can be more than one) like
+//
+// godotenv.Overload("fileone", "filetwo")
+//
+// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars.
+func Overload(filenames ...string) (err error) {
+ filenames = filenamesOrDefault(filenames)
+
+ for _, filename := range filenames {
+ err = loadFile(filename, true)
+ if err != nil {
+ return // return early on a spazout
+ }
+ }
+ return
+}
+
+// Read all env (with same file loading semantics as Load) but return values as
+// a map rather than automatically writing values into env
+func Read(filenames ...string) (envMap map[string]string, err error) {
+ filenames = filenamesOrDefault(filenames)
+ envMap = make(map[string]string)
+
+ for _, filename := range filenames {
+ individualEnvMap, individualErr := readFile(filename)
+
+ if individualErr != nil {
+ err = individualErr
+ return // return early on a spazout
+ }
+
+ for key, value := range individualEnvMap {
+ envMap[key] = value
+ }
+ }
+
+ return
+}
+
+// Exec loads env vars from the specified filenames (empty map falls back to default)
+// then executes the cmd specified.
+//
+// Simply hooks up os.Stdin/err/out to the command and calls Run()
+//
+// If you want more fine grained control over your command it's recommended
+// that you use `Load()` or `Read()` and the `os/exec` package yourself.
+func Exec(filenames []string, cmd string, cmdArgs []string) error {
+ Load(filenames...)
+
+ command := exec.Command(cmd, cmdArgs...)
+ command.Stdin = os.Stdin
+ command.Stdout = os.Stdout
+ command.Stderr = os.Stderr
+ return command.Run()
+}
+
+func filenamesOrDefault(filenames []string) []string {
+ if len(filenames) == 0 {
+ return []string{".env"}
+ }
+ return filenames
+}
+
+func loadFile(filename string, overload bool) error {
+ envMap, err := readFile(filename)
+ if err != nil {
+ return err
+ }
+
+ for key, value := range envMap {
+ if os.Getenv(key) == "" || overload {
+ os.Setenv(key, value)
+ }
+ }
+
+ return nil
+}
+
+func readFile(filename string) (envMap map[string]string, err error) {
+ file, err := os.Open(filename)
+ if err != nil {
+ return
+ }
+ defer file.Close()
+
+ envMap = make(map[string]string)
+
+ var lines []string
+ scanner := bufio.NewScanner(file)
+ for scanner.Scan() {
+ lines = append(lines, scanner.Text())
+ }
+
+ for _, fullLine := range lines {
+ if !isIgnoredLine(fullLine) {
+ key, value, err := parseLine(fullLine)
+
+ if err == nil {
+ envMap[key] = value
+ }
+ }
+ }
+ return
+}
+
+func parseLine(line string) (key string, value string, err error) {
+ if len(line) == 0 {
+ err = errors.New("zero length string")
+ return
+ }
+
+ // ditch the comments (but keep quoted hashes)
+ if strings.Contains(line, "#") {
+ segmentsBetweenHashes := strings.Split(line, "#")
+ quotesAreOpen := false
+ var segmentsToKeep []string
+ for _, segment := range segmentsBetweenHashes {
+ if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 {
+ if quotesAreOpen {
+ quotesAreOpen = false
+ segmentsToKeep = append(segmentsToKeep, segment)
+ } else {
+ quotesAreOpen = true
+ }
+ }
+
+ if len(segmentsToKeep) == 0 || quotesAreOpen {
+ segmentsToKeep = append(segmentsToKeep, segment)
+ }
+ }
+
+ line = strings.Join(segmentsToKeep, "#")
+ }
+
+ // now split key from value
+ splitString := strings.SplitN(line, "=", 2)
+
+ if len(splitString) != 2 {
+ // try yaml mode!
+ splitString = strings.SplitN(line, ":", 2)
+ }
+
+ if len(splitString) != 2 {
+ err = errors.New("Can't separate key from value")
+ return
+ }
+
+ // Parse the key
+ key = splitString[0]
+ if strings.HasPrefix(key, "export") {
+ key = strings.TrimPrefix(key, "export")
+ }
+ key = strings.Trim(key, " ")
+
+ // Parse the value
+ value = splitString[1]
+ // trim
+ value = strings.Trim(value, " ")
+
+ // check if we've got quoted values
+ if strings.Count(value, "\"") == 2 || strings.Count(value, "'") == 2 {
+ // pull the quotes off the edges
+ value = strings.Trim(value, "\"'")
+
+ // expand quotes
+ value = strings.Replace(value, "\\\"", "\"", -1)
+ // expand newlines
+ value = strings.Replace(value, "\\n", "\n", -1)
+ }
+
+ return
+}
+
+func isIgnoredLine(line string) bool {
+ trimmedLine := strings.Trim(line, " \n\t")
+ return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
+}
diff --git a/vendor/github.com/joho/godotenv/wercker.yml b/vendor/github.com/joho/godotenv/wercker.yml
new file mode 100644
index 0000000..c716ac9
--- /dev/null
+++ b/vendor/github.com/joho/godotenv/wercker.yml
@@ -0,0 +1 @@
+box: pjvds/golang
diff --git a/vendor/vendor.json b/vendor/vendor.json
index a2ef711..c8d78b3 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -20,6 +20,12 @@
"revision": "8ee79997227bf9b34611aee7946ae64735e6fd93",
"revisionTime": "2016-11-17T03:31:26Z"
},
+ {
+ "checksumSHA1": "ljZrmD7pmMXAkGNfp6IEzj31fY8=",
+ "path": "github.com/joho/godotenv",
+ "revision": "4ed13390c0acd2ff4e371e64d8b97c8954138243",
+ "revisionTime": "2015-09-07T01:02:28Z"
+ },
{
"checksumSHA1": "bKMZjd2wPw13VwoE7mBeSv5djFA=",
"path": "github.com/matttproud/golang_protobuf_extensions/pbutil",