├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── api_request_builder.go ├── bin └── .gitkeep ├── circle.yml ├── cmd └── linenotcat │ └── linenotcat.go ├── config_reader.go ├── devtools └── .gitkeep ├── glide.lock ├── glide.yaml ├── http_response_checker.go ├── main.go ├── notifier.go ├── queue.go ├── reader.go ├── status.go ├── stream.go └── tmpfile_writer.go /.gitignore: -------------------------------------------------------------------------------- 1 | /linenotcat 2 | bin/* 3 | !bin/.gitkeep 4 | devtools/* 5 | !devtools/.gitkeep 6 | /vendor/ 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2016 moznion, http://moznion.net/ 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the “Software”), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOVERSION=$(shell go version) 2 | GOOS=$(word 1,$(subst /, ,$(lastword $(GOVERSION)))) 3 | GOARCH=$(word 2,$(subst /, ,$(lastword $(GOVERSION)))) 4 | RELEASE_DIR=bin 5 | DEVTOOL_DIR=devtools 6 | PACKAGE=github.com/moznion/linenotcat 7 | REVISION=$(shell git rev-parse --verify HEAD) 8 | HAVE_GLIDE:=$(shell which glide > /dev/null 2>&1) 9 | 10 | .PHONY: clean build build-linux-amd64 build-linux-386 build-darwin-amd64 build-darwin-386 build-windows-amd64 build-windows-386 $(RELEASE_DIR)/linenotcat_$(GOOS)_$(GOARCH) all 11 | 12 | all: installdeps build-linux-amd64 build-linux-386 build-darwin-amd64 build-darwin-386 build-windows-amd64 build-windows-386 13 | 14 | build: $(RELEASE_DIR)/linenotcat_$(GOOS)_$(GOARCH) 15 | 16 | build-linux-amd64: 17 | @$(MAKE) build GOOS=linux GOARCH=amd64 18 | 19 | build-linux-386: 20 | @$(MAKE) build GOOS=linux GOARCH=386 21 | 22 | build-darwin-amd64: 23 | @$(MAKE) build GOOS=darwin GOARCH=amd64 24 | 25 | build-darwin-386: 26 | @$(MAKE) build GOOS=darwin GOARCH=386 27 | 28 | build-windows-amd64: 29 | @$(MAKE) build GOOS=windows GOARCH=amd64 30 | 31 | build-windows-386: 32 | @$(MAKE) build GOOS=windows GOARCH=386 33 | 34 | $(RELEASE_DIR)/linenotcat_$(GOOS)_$(GOARCH): 35 | ifndef VERSION 36 | @echo '[ERROR] $$VERSION must be specified' 37 | exit 255 38 | endif 39 | go build -ldflags "-X $(PACKAGE).rev=$(REVISION) -X $(PACKAGE).ver=$(VERSION)" \ 40 | -o $(RELEASE_DIR)/linenotcat_$(GOOS)_$(GOARCH)_$(VERSION) cmd/linenotcat/linenotcat.go 41 | 42 | $(DEVTOOL_DIR)/$(GOOS)/$(GOARCH)/glide: 43 | ifndef HAVE_GLIDE 44 | @echo "Installing glide for $(GOOS)/$(GOARCH)..." 45 | mkdir -p $(DEVTOOL_DIR)/$(GOOS)/$(GOARCH) 46 | wget -q -O - https://github.com/Masterminds/glide/releases/download/v0.12.3/glide-v0.12.3-$(GOOS)-$(GOARCH).tar.gz | tar xvz 47 | mv $(GOOS)-$(GOARCH)/glide $(DEVTOOL_DIR)/$(GOOS)/$(GOARCH)/glide 48 | rm -rf $(GOOS)-$(GOARCH) 49 | endif 50 | 51 | glide: $(DEVTOOL_DIR)/$(GOOS)/$(GOARCH)/glide 52 | 53 | installdeps: glide 54 | @PATH=$(DEVTOOL_DIR)/$(GOOS)/$(GOARCH):$(PATH) glide install 55 | 56 | clean: 57 | rm -rf $(RELEASE_DIR)/linenotcat_* 58 | 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | linenotcat 2 | == 3 | 4 | A command line tool to send messages to [LINE Notify](https://notify-bot.line.me/). 5 | 6 | linenotcat = LINE NOTify + cat 7 | 8 | Getting Started 9 | -- 10 | 11 | ### 1. Put configuration file 12 | 13 | ```sh 14 | $ echo 'YOUR_ACCESS_TOKEN' > $HOME/.linenotcat 15 | ``` 16 | 17 | It is deadly simple; this configuration file contains only access token. 18 | 19 | Default, `linenotcat` reads configuration file that is on `$HOME` directory. 20 | If you want to force it to read arbitrary file, please run the command with `--config_file` option. 21 | 22 | ### 2. Send message via STDIN 23 | 24 | ```sh 25 | $ echo 'Hello world!' | linenotcat 26 | ``` 27 | 28 | ### 2'. Send contents of file 29 | 30 | ```sh 31 | $ linenotcat ./your/awesome/file.txt 32 | ``` 33 | 34 | Options 35 | -- 36 | 37 | ``` 38 | Application Options: 39 | -m, --message= Send a text message directly 40 | -i, --image= Send an image file 41 | -t, --tee Print STDIN to screen before posting (false) 42 | -s, --stream Post messages to LINE Notify continuously (false) 43 | --config_file= Load the specified configuration file 44 | --status Show connection status that belongs to the token (false) 45 | 46 | Help Options: 47 | -h, --help Show this help message 48 | ``` 49 | 50 | ### -m, --message 51 | 52 | Send a text message directly. 53 | 54 | e.g. 55 | 56 | ```sh 57 | $ linenotcat -m 'Hello world!' 58 | ``` 59 | 60 | Then `Hello world!` text will be sent via LINE Notify. 61 | 62 | ### -i, --image 63 | 64 | Send an image file. 65 | 66 | e.g. 67 | 68 | ```sh 69 | $ linenotcat -i /path/to/your/awesome/image.png 70 | ``` 71 | 72 | Then `image.png` image will be sent via LINE Notify with default message (default message=`Image file`). 73 | 74 | If you want to send an image with arbitrary message, please use with `-m` option. 75 | 76 | e.g. 77 | 78 | ```sh 79 | $ linenotcat -i /path/to/your/awesome/image.png -m 'Super duper image!' 80 | ``` 81 | 82 | ### -t, --tee 83 | 84 | Print STDIN to screen before posting. 85 | 86 | ### -s, --stream 87 | 88 | Stream messages to LINE Notify continuously. 89 | 90 | e.g. 91 | 92 | ```sh 93 | $ tail -f /your/awesome/error.log | linenotcat --stream 94 | ``` 95 | 96 | Then contents of `error.log` are notified continuously to LINE Notify until process is died. 97 | 98 | ### --config_file 99 | 100 | e.g. 101 | 102 | ```sh 103 | $ echo 'Hello world!' | linenotcat --config_file="/your/path/to/config" 104 | ``` 105 | 106 | Then this command loads token information from your specified configuration file. 107 | 108 | ### --status 109 | 110 | Show connection status that belongs to the token. 111 | 112 | e.g. 113 | 114 | ```sh 115 | $ linenotcat --status 116 | {"status":200,"message":"ok","targetType":"USER","target":"moznion"} 117 | ``` 118 | 119 | Homebrew 120 | -- 121 | 122 | You can get this tool via homebrew. 123 | 124 | ``` 125 | $ brew tap moznion/homebrew-linenotcat 126 | $ brew install linenotcat 127 | ``` 128 | 129 | Executable Binaries 130 | -- 131 | 132 | Those are on [GitHub Releases](https://github.com/moznion/linenotcat/releases) 133 | 134 | For developers 135 | -- 136 | 137 | This project depends on [glide](https://github.com/Masterminds/glide). 138 | So if you want to build this project, please build with glide. 139 | 140 | Example: 141 | 142 | ```sh 143 | $ glide install 144 | $ go build cmd/linenotcat/linenotcat.go 145 | ``` 146 | 147 | Or you can build with `make` command 148 | 149 | ```sh 150 | $ make VERSION=1.2.3 151 | ``` 152 | 153 | Thanks 154 | -- 155 | 156 | This tool is much inspired by [slackcat](https://github.com/vektorlab/slackcat) and some code is taken from that. 157 | 158 | See Also 159 | -- 160 | 161 | - [LINE Notify](https://notify-bot.line.me/) 162 | 163 | Author 164 | -- 165 | 166 | moznion () 167 | 168 | License 169 | -- 170 | 171 | ``` 172 | The MIT License (MIT) 173 | Copyright © 2016 moznion, http://moznion.net/ 174 | 175 | Permission is hereby granted, free of charge, to any person obtaining a copy 176 | of this software and associated documentation files (the “Software”), to deal 177 | in the Software without restriction, including without limitation the rights 178 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 179 | copies of the Software, and to permit persons to whom the Software is 180 | furnished to do so, subject to the following conditions: 181 | 182 | The above copyright notice and this permission notice shall be included in 183 | all copies or substantial portions of the Software. 184 | 185 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 186 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 187 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 188 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 189 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 190 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 191 | THE SOFTWARE. 192 | ``` 193 | 194 | -------------------------------------------------------------------------------- /api_request_builder.go: -------------------------------------------------------------------------------- 1 | package linenotcat 2 | 3 | import ( 4 | "io" 5 | "mime/multipart" 6 | "net/http" 7 | ) 8 | 9 | const ( 10 | baseURL = "https://notify-api.line.me/api" 11 | ) 12 | 13 | type apiRequestBuilder struct { 14 | token string 15 | } 16 | 17 | func (arb *apiRequestBuilder) buildFormNotifyRequest(body io.Reader) (*http.Request, error) { 18 | return arb.buildNotifyRequest(body, "application/x-www-form-urlencoded") 19 | } 20 | 21 | func (arb *apiRequestBuilder) buildMultipartNotifyRequest(body io.Reader, w *multipart.Writer) (*http.Request, error) { 22 | return arb.buildNotifyRequest(body, w.FormDataContentType()) 23 | } 24 | 25 | func (arb *apiRequestBuilder) buildNotifyRequest(body io.Reader, contentType string) (*http.Request, error) { 26 | req, err := http.NewRequest( 27 | "POST", 28 | baseURL+"/notify", 29 | body, 30 | ) 31 | 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | req.Header.Set("Authorization", "Bearer "+arb.token) 37 | req.Header.Set("Content-Type", contentType) 38 | 39 | return req, nil 40 | } 41 | 42 | func (arb *apiRequestBuilder) buildStatusRequest() (*http.Request, error) { 43 | req, err := http.NewRequest( 44 | "GET", 45 | baseURL+"/status", 46 | nil, 47 | ) 48 | 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | req.Header.Set("Authorization", "Bearer "+arb.token) 54 | 55 | return req, nil 56 | } 57 | -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moznion/linenotcat/0474cde65b680972baff4d211ae0ce19a78be59c/bin/.gitkeep -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | environment: 3 | GODIST: "go1.7.3.linux-amd64.tar.gz" 4 | post: 5 | - mkdir -p downloads 6 | - test -e downloads/$GODIST || curl -o downloads/$GODIST https://storage.googleapis.com/golang/$GODIST 7 | - sudo rm -rf /usr/local/go 8 | - sudo tar -C /usr/local -xzf downloads/$GODIST 9 | deployment: 10 | release: 11 | tag: /[0-9]+\.[0-9]+\.[0-9]+/ 12 | commands: 13 | - go get github.com/tcnksm/ghr 14 | - make clean 15 | - make VERSION=`git describe --tags | perl -anlE 'm/\A([^\-]+)-?/; print $1'` 16 | - rm bin/.gitkeep 17 | - ghr -t $GITHUB_TOKEN -u $CIRCLE_PROJECT_USERNAME -r $CIRCLE_PROJECT_REPONAME --replace `git describe --tags | perl -anlE 'm/\A([^\-]+)-?/; print $1'` bin/ 18 | 19 | -------------------------------------------------------------------------------- /cmd/linenotcat/linenotcat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/moznion/linenotcat" 7 | ) 8 | 9 | func main() { 10 | linenotcat.Run(os.Args[1:]) 11 | } 12 | -------------------------------------------------------------------------------- /config_reader.go: -------------------------------------------------------------------------------- 1 | package linenotcat 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | ) 10 | 11 | func readToken(configFilePath string) (string, error) { 12 | fp, err := os.Open(configFilePath) 13 | if err != nil { 14 | return "", err 15 | } 16 | 17 | var token string 18 | 19 | scanner := bufio.NewScanner(fp) 20 | for scanner.Scan() { 21 | token = scanner.Text() 22 | if token != "" { 23 | break 24 | } 25 | } 26 | err = scanner.Err() 27 | if err != nil { 28 | return "", err 29 | } 30 | 31 | return token, nil 32 | } 33 | 34 | func readDefaultToken() (string, error) { 35 | defaultConfigFilePath, err := getDefaultConfigFilePath() 36 | if err != nil { 37 | return "", err 38 | } 39 | return readToken(defaultConfigFilePath) 40 | } 41 | 42 | func getDefaultConfigFilePath() (string, error) { 43 | var homedir string 44 | if runtime.GOOS == "windows" { 45 | if homedir = os.Getenv("USERPROFILE"); homedir == "" { 46 | return "", errors.New(`%USERPROFILE% not set`) 47 | } 48 | } else { 49 | if homedir = os.Getenv("HOME"); homedir == "" { 50 | return "", errors.New(`$HOME not set`) 51 | } 52 | } 53 | return filepath.Join(homedir, ".linenotcat"), nil 54 | } 55 | -------------------------------------------------------------------------------- /devtools/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moznion/linenotcat/0474cde65b680972baff4d211ae0ce19a78be59c/devtools/.gitkeep -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 9f77fe59419cd80a0b2b9c884aa607e66e10851b949074924b170dfde0d00b55 2 | updated: 2017-10-10T19:02:42.021473441+09:00 3 | imports: 4 | - name: github.com/jessevdk/go-flags 5 | version: 96dc06278ce32a0e9d957d590bb987c81ee66407 6 | testImports: [] 7 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/moznion/linenotcat 2 | import: 3 | - package: github.com/jessevdk/go-flags 4 | version: v1.3.0 5 | 6 | -------------------------------------------------------------------------------- /http_response_checker.go: -------------------------------------------------------------------------------- 1 | package linenotcat 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | ) 8 | 9 | func checkHTTPStatus(res *http.Response) error { 10 | statusCode := res.StatusCode 11 | if statusCode < 200 || statusCode > 299 { 12 | read, _ := ioutil.ReadAll(res.Body) 13 | return fmt.Errorf(string(read)) 14 | } 15 | 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package linenotcat 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | 8 | "github.com/jessevdk/go-flags" 9 | ) 10 | 11 | var ( 12 | ver string 13 | rev string 14 | ) 15 | 16 | type opts struct { 17 | Message string `short:"m" long:"message" default:"" description:"Send a text message directly"` 18 | ImageFile string `short:"i" long:"image" default:"" description:"Send an image file"` 19 | Tee bool `short:"t" long:"tee" description:"Print STDIN to screen before posting"` 20 | Stream bool `short:"s" long:"stream" description:"Post messages to LINE Notify continuously"` 21 | ConfigFile string `long:"config_file" default:"" description:"Load the specified configuration file"` 22 | Status bool `long:"status" description:"Show connection status that belongs to the token"` 23 | } 24 | 25 | func parseArgs(args []string) (opt *opts, remainArgs []string) { 26 | o := &opts{} 27 | p := flags.NewParser(o, flags.Default) 28 | p.Usage = fmt.Sprintf("\n\nVersion: %s\nRevision: %s", ver, rev) 29 | remainArgs, err := p.ParseArgs(args) 30 | if err != nil { 31 | os.Exit(1) 32 | } 33 | return o, remainArgs 34 | } 35 | 36 | func Run(args []string) { 37 | o, remainArgs := parseArgs(args) 38 | 39 | var token string 40 | var err error 41 | if o.ConfigFile == "" { 42 | token, err = readDefaultToken() 43 | } else { 44 | token, err = readToken(o.ConfigFile) 45 | } 46 | 47 | if err != nil { 48 | fmt.Fprintf(os.Stderr, `[ERROR] Failed to load configuration file: %v 49 | Is configuration file perhaps missing? 50 | Please try:`, err) 51 | if runtime.GOOS == "windows" { 52 | fmt.Fprintln(os.Stderr, ` C:\>echo 'YOUR_ACCESS_TOKEN' > %USERPROFILE%\.linenotcat`) 53 | } else { 54 | fmt.Fprintln(os.Stderr, ` $ echo 'YOUR_ACCESS_TOKEN' > $HOME/.linenotcat`) 55 | } 56 | os.Exit(1) 57 | } 58 | 59 | arb := &apiRequestBuilder{token: token} 60 | 61 | if o.Status { 62 | status := &status{apiRequestBuilder: arb} 63 | err := status.getStatus() 64 | if err != nil { 65 | panic(err) 66 | } 67 | return 68 | } 69 | 70 | ln := &lineNotifier{ 71 | apiRequestBuilder: arb, 72 | } 73 | 74 | if o.ImageFile != "" { 75 | warnIfStreamMode(o) 76 | warnIfArgumentRemained(remainArgs) 77 | 78 | msg := o.Message 79 | if msg == "" { 80 | msg = "Image file" 81 | } 82 | err := ln.notifyImage(o.ImageFile, msg, o.Tee) 83 | if err != nil { 84 | panic(err) 85 | } 86 | return 87 | } 88 | 89 | if o.Message != "" { 90 | // Send text message directly 91 | warnIfStreamMode(o) 92 | ln.notifyMessage(o.Message, o.Tee) 93 | return 94 | } 95 | 96 | if o.Stream { 97 | // Stream mode 98 | warnIfArgumentRemained(remainArgs) 99 | 100 | s := newStream(ln) 101 | go s.processStreamQueue(o.Tee) 102 | go s.watchStdin() 103 | go s.trap() 104 | select {} 105 | 106 | return 107 | } 108 | 109 | if len(remainArgs) > 0 { 110 | // Send file contents 111 | warnIfStreamMode(o) 112 | ln.notifyFile(remainArgs[0], o.Tee) 113 | return 114 | } 115 | 116 | // Send messages from STDIN 117 | lines := make(chan string) 118 | go readFromStdin(lines) 119 | 120 | tmpFilePath, err := writeTemp(lines) 121 | if err != nil { 122 | panic(err) 123 | } 124 | 125 | defer os.Remove(tmpFilePath) 126 | ln.notifyFile(tmpFilePath, o.Tee) 127 | } 128 | 129 | func warnIfStreamMode(o *opts) { 130 | if o.Stream { 131 | fmt.Println("Given stream option, but it is ignored when image sending mode") 132 | } 133 | } 134 | 135 | func warnIfArgumentRemained(remainArgs []string) { 136 | if len(remainArgs) > 0 { 137 | fmt.Println("Given file, but it is ignored when stream mode") 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /notifier.go: -------------------------------------------------------------------------------- 1 | package linenotcat 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "mime/multipart" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "strings" 13 | ) 14 | 15 | type lineNotifier struct { 16 | apiRequestBuilder *apiRequestBuilder 17 | } 18 | 19 | func (l *lineNotifier) notifyMessage(msg string, tee bool) error { 20 | values := url.Values{} 21 | values.Set("message", msg) 22 | 23 | req, err := l.apiRequestBuilder.buildFormNotifyRequest(strings.NewReader(values.Encode())) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | if tee { 29 | fmt.Print(msg) 30 | } 31 | 32 | res, err := http.DefaultClient.Do(req) 33 | defer res.Body.Close() 34 | if err != nil { 35 | return err 36 | } 37 | 38 | return checkHTTPStatus(res) 39 | } 40 | 41 | func (l *lineNotifier) notifyMessages(msgs []string, tee bool) error { 42 | return l.notifyMessage(strings.Join(msgs, "\n"), tee) 43 | } 44 | 45 | func (l *lineNotifier) notifyFile(tmpFilePath string, tee bool) error { 46 | msg, err := ioutil.ReadFile(tmpFilePath) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | return l.notifyMessage(string(msg), tee) 52 | } 53 | 54 | func (l *lineNotifier) notifyImage(imageFilePath, message string, tee bool) error { 55 | body := &bytes.Buffer{} 56 | mw := multipart.NewWriter(body) 57 | 58 | mw.WriteField("message", message) 59 | if tee { 60 | fmt.Print(message) 61 | } 62 | 63 | pw, err := mw.CreateFormFile("imageFile", imageFilePath) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | file, err := os.Open(imageFilePath) 69 | if err != nil { 70 | return err 71 | } 72 | defer file.Close() 73 | 74 | _, err = io.Copy(pw, file) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | err = mw.Close() 80 | if err != nil { 81 | return err 82 | } 83 | 84 | req, err := l.apiRequestBuilder.buildMultipartNotifyRequest(body, mw) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | res, err := http.DefaultClient.Do(req) 90 | defer res.Body.Close() 91 | if err != nil { 92 | return err 93 | } 94 | 95 | return checkHTTPStatus(res) 96 | } 97 | -------------------------------------------------------------------------------- /queue.go: -------------------------------------------------------------------------------- 1 | package linenotcat 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type queue struct { 8 | queue []string 9 | lock sync.RWMutex 10 | } 11 | 12 | func newQueue() *queue { 13 | return &queue{ 14 | queue: []string{}, 15 | lock: sync.RWMutex{}, 16 | } 17 | } 18 | 19 | func (q *queue) add(line string) { 20 | q.lock.Lock() 21 | q.queue = append(q.queue, line) 22 | q.lock.Unlock() 23 | } 24 | 25 | func (q *queue) isEmpty() bool { 26 | return (len(q.queue) < 1) 27 | } 28 | 29 | func (q *queue) flush() []string { 30 | q.lock.Lock() 31 | items := q.queue 32 | q.queue = []string{} 33 | q.lock.Unlock() 34 | return items 35 | } 36 | -------------------------------------------------------------------------------- /reader.go: -------------------------------------------------------------------------------- 1 | package linenotcat 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | ) 7 | 8 | func readFromStdin(lines chan string) { 9 | scanner := bufio.NewScanner(os.Stdin) 10 | scanner.Split(bufio.ScanLines) 11 | for scanner.Scan() { 12 | lines <- scanner.Text() 13 | } 14 | close(lines) 15 | } 16 | -------------------------------------------------------------------------------- /status.go: -------------------------------------------------------------------------------- 1 | package linenotcat 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | ) 8 | 9 | type status struct { 10 | apiRequestBuilder *apiRequestBuilder 11 | } 12 | 13 | func (s *status) getStatus() error { 14 | req, err := s.apiRequestBuilder.buildStatusRequest() 15 | if err != nil { 16 | return err 17 | } 18 | 19 | res, err := http.DefaultClient.Do(req) 20 | if err != nil { 21 | return err 22 | } 23 | defer res.Body.Close() 24 | read, err := ioutil.ReadAll(res.Body) 25 | if err != nil { 26 | return err 27 | } 28 | fmt.Println(string(read)) 29 | 30 | return checkHTTPStatus(res) 31 | } 32 | -------------------------------------------------------------------------------- /stream.go: -------------------------------------------------------------------------------- 1 | package linenotcat 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | ) 8 | 9 | const ( 10 | interval = 3 * time.Second 11 | ) 12 | 13 | type stream struct { 14 | queue *queue 15 | shutdown chan os.Signal 16 | lineNotifier *lineNotifier 17 | } 18 | 19 | func newStream(lineNotifier *lineNotifier) *stream { 20 | return &stream{ 21 | queue: newQueue(), 22 | shutdown: make(chan os.Signal, 1), 23 | lineNotifier: lineNotifier, 24 | } 25 | } 26 | 27 | func (s *stream) trap() { 28 | sigcount := 0 29 | for sig := range s.shutdown { 30 | if sigcount > 0 { 31 | fmt.Println("Aborted") 32 | os.Exit(1) 33 | } 34 | fmt.Printf("Got signal: %s\n", sig.String()) 35 | fmt.Println("Press Ctrl+C again to exit immediately") 36 | sigcount++ 37 | go s.exit() 38 | } 39 | } 40 | 41 | func (s *stream) exit() { 42 | for { 43 | if s.queue.isEmpty() { 44 | os.Exit(0) 45 | } else { 46 | fmt.Println("Flushing remaining messages...") 47 | time.Sleep(interval) 48 | } 49 | } 50 | } 51 | 52 | func (s *stream) processStreamQueue(tee bool) { 53 | if !s.queue.isEmpty() { 54 | lines := s.queue.flush() 55 | s.lineNotifier.notifyMessages(lines, tee) 56 | } 57 | time.Sleep(interval) 58 | s.processStreamQueue(tee) 59 | } 60 | 61 | func (s *stream) watchStdin() { 62 | for _ = range time.Tick(interval) { 63 | lines := make(chan string) 64 | go readFromStdin(lines) 65 | for line := range lines { 66 | s.queue.add(line) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tmpfile_writer.go: -------------------------------------------------------------------------------- 1 | package linenotcat 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | ) 9 | 10 | func writeTemp(lines chan string) (string, error) { 11 | tmp, err := ioutil.TempFile(os.TempDir(), "linenotcat-") 12 | if err != nil { 13 | return "", err 14 | } 15 | defer tmp.Close() 16 | 17 | w := bufio.NewWriter(tmp) 18 | for line := range lines { 19 | fmt.Fprintln(w, line) 20 | } 21 | w.Flush() 22 | 23 | return tmp.Name(), nil 24 | } 25 | --------------------------------------------------------------------------------