Browse Source

Create short url, refactor quite a bit

master
Tyler Sommer 2 years ago
parent
commit
addc00f538
Signed by: tyler-sommer GPG Key ID: C09C010500DBD008
8 changed files with 373 additions and 124 deletions
  1. +1
    -0
      .gitignore
  2. +10
    -2
      README.md
  3. +29
    -0
      cmd/spaced/fs.go
  4. +186
    -0
      cmd/spaced/main.go
  5. +73
    -0
      cmd/spaced/s3.go
  6. +52
    -0
      cmd/spaced/urls.go
  7. +22
    -0
      config.toml.dist
  8. +0
    -122
      main.go

+ 1
- 0
.gitignore View File

@ -1 +1,2 @@
.idea/
config.toml

+ 10
- 2
README.md View File

@ -1,8 +1,16 @@
spaced
======
Monitor a directory, uploading newly created files to a pre-configured
S3 bucket and creating a shareable link.
Screenshot auto uploader that puts a short link to the newly uploaded image in
the system clipboard.
Overview
--------
spaced monitors a directory, uploading newly created files to a pre-configured
S3-compatible store. After an image is uploaded, a short URL is generated by
[eok.vin](https://github.com/veonik/eokvin) and placed in the system clipboard.
Usage


+ 29
- 0
cmd/spaced/fs.go View File

@ -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
}

+ 186
- 0
cmd/spaced/main.go View File

@ -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)
}
}
}

+ 73
- 0
cmd/spaced/s3.go View File

@ -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
}

+ 52
- 0
cmd/spaced/urls.go View File

@ -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)
}
}

+ 22
- 0
config.toml.dist View File

@ -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"

+ 0
- 122
main.go View File

@ -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…
Cancel
Save