mirror of https://github.com/veonik/spaced
parent
ee20bcae79
commit
addc00f538
@ -1 +1,2 @@
|
||||
.idea/
|
||||
config.toml
|
||||
|
@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/mattes/go-expand-tilde.v1"
|
||||
)
|
||||
|
||||
type fswatchConfig struct {
|
||||
MonitorPath string `toml:"monitor_path"`
|
||||
}
|
||||
|
||||
func (c fswatchConfig) GetService() (*fsnotify.Watcher, error) {
|
||||
if len(c.MonitorPath) == 0 {
|
||||
return nil, errors.New("monitor_path cannot be blank")
|
||||
}
|
||||
var err error
|
||||
if c.MonitorPath, err = tilde.Expand(c.MonitorPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = w.Add(c.MonitorPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return w, nil
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
|
||||
var config struct {
|
||||
S3 s3Config `toml:"s3"`
|
||||
URL urlShortenerConfig `toml:"url"`
|
||||
FS fswatchConfig `toml:"fswatch"`
|
||||
|
||||
LogFile string
|
||||
}
|
||||
|
||||
func init() {
|
||||
configFile := ""
|
||||
|
||||
flag.Usage = func() {
|
||||
fmt.Println("Usage: spaced [options]")
|
||||
fmt.Println()
|
||||
fmt.Println("Command spaced automatically uploads screenshots and puts a shareable URL in")
|
||||
fmt.Println("the system clipboard. The screenshots are uploaded to an S3-compatible bucket,")
|
||||
fmt.Println("and eokvin as a URL shortening service.")
|
||||
fmt.Println()
|
||||
fmt.Println("spaced works with an S3-compatible storage, such as DigitalOcean Spaces.")
|
||||
fmt.Println()
|
||||
fmt.Println("See: https://github.com/veonik/eokvin")
|
||||
fmt.Println()
|
||||
fmt.Println("Options:")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
flag.StringVar(&config.S3.AccessKey, "access-key", "", "Access key (required)")
|
||||
flag.StringVar(&config.S3.SecretKey, "secret-key", "", "Secret key (required)")
|
||||
flag.StringVar(&config.S3.Endpoint, "endpoint", "", "Endpoint URL (required)")
|
||||
flag.StringVar(&config.S3.Bucket, "bucket", "", "Bucket name (required)")
|
||||
flag.StringVar(&config.S3.Prefix, "prefix", "", "Key prefix")
|
||||
flag.StringVar(&config.FS.MonitorPath, "monitor-path", "~/Desktop", "Path to monitor")
|
||||
flag.StringVar(&config.LogFile, "log-file", "", "Log file path. If blank, logs print to stdout")
|
||||
flag.StringVar(&configFile, "config-file", "config.toml", "Config file path")
|
||||
|
||||
var eokvinToken string
|
||||
var eokvinEndpoint string
|
||||
flag.StringVar(&eokvinToken, "token", "", "Secret token for eokvin service")
|
||||
flag.StringVar(&eokvinEndpoint, "eokvin", "", "URL shortener service endpoint")
|
||||
flag.Parse()
|
||||
|
||||
if config.LogFile != "" {
|
||||
f, err := os.OpenFile(config.LogFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.FileMode(0644))
|
||||
if err != nil {
|
||||
log.Fatal("unable to open spaced.log for writing:", err.Error())
|
||||
}
|
||||
log.SetOutput(f)
|
||||
}
|
||||
|
||||
if configFile != "" {
|
||||
if _, err := toml.DecodeFile(configFile, &config); err != nil {
|
||||
log.Fatal("unable to decode config file:", err.Error())
|
||||
}
|
||||
}
|
||||
if eokvinToken != "" {
|
||||
config.URL.Options["token"] = eokvinToken
|
||||
}
|
||||
if eokvinEndpoint != "" {
|
||||
config.URL.Options["endpoint"] = eokvinEndpoint
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
stop := make(chan struct{})
|
||||
sigrec := make(chan os.Signal)
|
||||
signal.Notify(sigrec, os.Kill, os.Interrupt)
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
worker(stop)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-stop:
|
||||
wg.Wait()
|
||||
os.Exit(0)
|
||||
case <-sigrec:
|
||||
close(stop)
|
||||
}
|
||||
}
|
||||
|
||||
func worker(stop chan struct{}) {
|
||||
s3, err := config.S3.GetService()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
c, err := config.URL.GetService()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
watcher, err := config.FS.GetService()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := watcher.Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case event.Op&fsnotify.Create == fsnotify.Create:
|
||||
f := filepath.Base(event.Name)
|
||||
if strings.HasPrefix(f, ".") {
|
||||
log.Println("new file created, not uploading:", f)
|
||||
continue
|
||||
} else if !strings.HasSuffix(f, ".png") {
|
||||
log.Println("non png created, not uploading:", f)
|
||||
continue
|
||||
}
|
||||
_, err := s3.FPutObject(f, event.Name)
|
||||
if err != nil {
|
||||
log.Println("error writing to storage:", err.Error())
|
||||
continue
|
||||
}
|
||||
u, err := s3.PresignedGetObject(f, 30*time.Minute, url.Values{})
|
||||
if err != nil {
|
||||
log.Println("error getting public aws url:", err.Error())
|
||||
continue
|
||||
}
|
||||
log.Println("AWS URL:", u.String())
|
||||
su, err := c.NewShortURL(u)
|
||||
if err != nil {
|
||||
log.Println("error getting short share url:", err.Error())
|
||||
continue
|
||||
}
|
||||
log.Println("Share URL:", su.String())
|
||||
cmd := exec.Command("pbcopy")
|
||||
p, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
log.Println("error opening clipboard:", err.Error())
|
||||
continue
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Println("error running clipboard command:", err.Error())
|
||||
continue
|
||||
}
|
||||
if _, err := fmt.Fprintf(p, "%s", su.String()); err != nil {
|
||||
log.Println("error writing to clipboard:", err.Error())
|
||||
}
|
||||
if err := p.Close(); err != nil {
|
||||
log.Println("error closing clipboard:", err.Error())
|
||||
}
|
||||
if err := cmd.Wait(); err != nil {
|
||||
log.Println("clipboard command exited with error:", err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
close(stop)
|
||||
return
|
||||
}
|
||||
log.Println("error:", err)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/minio/minio-go"
|
||||
"github.com/pkg/errors"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type s3Kind int
|
||||
const (
|
||||
s3KindMinio s3Kind = iota
|
||||
)
|
||||
|
||||
type s3Config struct {
|
||||
Kind s3Kind `toml:"kind"`
|
||||
|
||||
AccessKey string `toml:"access_key"`
|
||||
SecretKey string `toml:"secret_key"`
|
||||
Endpoint string `toml:"endpoint"`
|
||||
Bucket string `toml:"bucket"`
|
||||
Prefix string `toml:"prefix"`
|
||||
}
|
||||
|
||||
type s3Client interface {
|
||||
FPutObject(objectName, filePath string) (n int64, err error)
|
||||
PresignedGetObject(objectName string, expires time.Duration, reqParams url.Values) (u *url.URL, err error)
|
||||
}
|
||||
|
||||
type minioS3Client struct {
|
||||
*minio.Client
|
||||
|
||||
Bucket string
|
||||
Prefix string
|
||||
}
|
||||
|
||||
func (c *minioS3Client) FPutObject(objectName, filePath string) (n int64, err error) {
|
||||
f := filepath.Join(c.Prefix, objectName)
|
||||
return c.Client.FPutObject(c.Bucket, f, filePath, minio.PutObjectOptions{})
|
||||
}
|
||||
|
||||
func (c *minioS3Client) PresignedGetObject(objectName string, expires time.Duration, reqParams url.Values) (u *url.URL, err error) {
|
||||
f := filepath.Join(c.Prefix, objectName)
|
||||
return c.Client.PresignedGetObject(c.Bucket, f, expires, reqParams)
|
||||
}
|
||||
|
||||
func (c s3Config) GetService() (s3Client, error) {
|
||||
if c.Kind != s3KindMinio {
|
||||
return nil, errors.Errorf("unsupported S3 kind: %s", c.Kind)
|
||||
}
|
||||
if len(c.AccessKey) == 0 {
|
||||
return nil, errors.New("access-key cannot be blank")
|
||||
}
|
||||
if len(c.SecretKey) == 0 {
|
||||
return nil, errors.New("secret-key cannot be blank")
|
||||
}
|
||||
if len(c.Endpoint) == 0 {
|
||||
return nil, errors.New("endpoint cannot be blank")
|
||||
}
|
||||
if len(c.Bucket) == 0 {
|
||||
return nil, errors.New("bucket cannot be blank")
|
||||
}
|
||||
cl, err := minio.New(c.Endpoint, c.AccessKey, c.SecretKey, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &minioS3Client{
|
||||
Client: cl,
|
||||
Bucket: c.Bucket,
|
||||
Prefix: c.Prefix,
|
||||
}, nil
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/veonik/eokvin"
|
||||
)
|
||||
|
||||
type urlsKind int
|
||||
const (
|
||||
urlsKindEokvin urlsKind = iota
|
||||
)
|
||||
|
||||
type urlShortenerConfig struct {
|
||||
Kind urlsKind
|
||||
Options map[string]string `toml:"options"`
|
||||
}
|
||||
|
||||
type urlShortener interface {
|
||||
NewShortURL(u *url.URL) (*url.URL, error)
|
||||
}
|
||||
|
||||
type eokvinShortener struct {
|
||||
*eokvin.Client
|
||||
}
|
||||
|
||||
func (c *eokvinShortener) NewShortURL(u *url.URL) (*url.URL, error) {
|
||||
su, err := c.Client.NewShortURL(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &su.URL, nil
|
||||
}
|
||||
|
||||
func (c urlShortenerConfig) GetService() (urlShortener, error) {
|
||||
switch c.Kind {
|
||||
case urlsKindEokvin:
|
||||
token := c.Options["token"]
|
||||
endpoint := c.Options["endpoint"]
|
||||
if token == "" || endpoint == "" {
|
||||
return nil, errors.New("options 'token' and 'endpoint' are required")
|
||||
}
|
||||
c := eokvin.NewClient(c.Options["token"])
|
||||
c.Endpoint = endpoint
|
||||
return &eokvinShortener{
|
||||
Client: c,
|
||||
}, nil
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported URL shortener kind %s", c.Kind)
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
# Filesystem options
|
||||
[fs]
|
||||
# Path to monitor for new files
|
||||
#monitor_path = "~/Desktop"
|
||||
|
||||
# S3 storage options
|
||||
[s3]
|
||||
# S3-compatible endpoint, host only
|
||||
endpoint = "s3.us-east-2.amazonaws.com"
|
||||
access_key = "aws access key id"
|
||||
secret_key = "aws secret key"
|
||||
# Images will be stored in this bucket only
|
||||
bucket = "aws bucket"
|
||||
# Images will have this prefix (treated as a path prefix)
|
||||
#prefix = ""
|
||||
|
||||
# URL shortener options
|
||||
[url.options]
|
||||
# Fully qualified path to eok.vin service's create URL endpoint
|
||||
endpoint = "https://eok.vin/new"
|
||||
# Plaintext secret token for eok.vin service
|
||||
token = "super secret eok.vin token"
|
@ -1,122 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/minio/minio-go"
|
||||
"gopkg.in/mattes/go-expand-tilde.v1"
|
||||
)
|
||||
|
||||
var accessKey string
|
||||
var secretKey string
|
||||
var endpoint string
|
||||
var bucket string
|
||||
var prefix string
|
||||
var monitorPath string
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&accessKey, "access-key", "", "Access key")
|
||||
flag.StringVar(&secretKey, "secret-key", "", "Secret key")
|
||||
flag.StringVar(&endpoint, "endpoint", "nyc3.digitaloceanspaces.com", "Endpoint URL")
|
||||
flag.StringVar(&bucket, "bucket", "veo", "Bucket (or Space) name")
|
||||
flag.StringVar(&prefix, "prefix", "", "Key prefix")
|
||||
flag.StringVar(&monitorPath, "monitor-path", "~/Desktop", "Path to monitor")
|
||||
flag.Parse()
|
||||
|
||||
if len(accessKey) == 0 {
|
||||
log.Fatal("access-key cannot be blank")
|
||||
}
|
||||
if len(secretKey) == 0 {
|
||||
log.Fatal("secret-key cannot be blank")
|
||||
}
|
||||
if len(endpoint) == 0 {
|
||||
log.Fatal("endpoint cannot be blank")
|
||||
}
|
||||
if len(bucket) == 0 {
|
||||
log.Fatal("bucket cannot be blank")
|
||||
}
|
||||
if len(monitorPath) == 0 {
|
||||
log.Fatal("monitor-path cannot be blank")
|
||||
}
|
||||
var err error
|
||||
if monitorPath, err = tilde.Expand(monitorPath); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func listObjects(client *minio.Client) {
|
||||
doneCh := make(chan struct{})
|
||||
defer close(doneCh)
|
||||
for o := range client.ListObjectsV2(bucket, prefix, true, doneCh) {
|
||||
if o.Err != nil {
|
||||
fmt.Println(o.Err)
|
||||
return
|
||||
}
|
||||
fmt.Println(o)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
client, err := minio.New(endpoint, accessKey, secretKey, true)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := watcher.Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Println("event:", event)
|
||||
switch {
|
||||
case event.Op&fsnotify.Write == fsnotify.Write:
|
||||
log.Println("modified file:", event.Name)
|
||||
case event.Op&fsnotify.Create == fsnotify.Create:
|
||||
f := filepath.Join(prefix, filepath.Base(event.Name))
|
||||
_, err := client.FPutObject(bucket, f, event.Name, minio.PutObjectOptions{})
|
||||
if err != nil {
|
||||
log.Printf("error writing to storage: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
u, err := client.PresignedGetObject(bucket, f, 20*time.Minute, url.Values{})
|
||||
if err != nil {
|
||||
log.Printf("error getting share url: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
fmt.Println("Share URL:", u.String())
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Println("error:", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go listObjects(client)
|
||||
|
||||
err = watcher.Add(monitorPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
<-done
|
||||
}
|
Loading…
Reference in new issue