From 4405fc9b8dfb8a6533c2e08093889a7580ef77ba Mon Sep 17 00:00:00 2001 From: Francesco La Camera Date: Tue, 2 Sep 2025 01:03:18 +0200 Subject: [PATCH] feat: add tasks --- README.md | 2 +- cmd/rover/main.go | 162 +++----------------- go.mod | 4 + go.sum | 11 ++ internal/config/config.go | 100 +++++++++++++ internal/email/email.go | 302 ++++++++++++++++++++++++++++++++++++++ internal/tasks/tasks.go | 70 +++++++++ internal/webdav/client.go | 106 +++++++++++++ rover.yml | 1 - 9 files changed, 611 insertions(+), 147 deletions(-) create mode 100644 internal/config/config.go create mode 100644 internal/email/email.go create mode 100644 internal/tasks/tasks.go create mode 100644 internal/webdav/client.go diff --git a/README.md b/README.md index cabc174..1f525c8 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ go install github.com/ruhrcloud/rover/cmd/rover@latest ## Configuration ```yaml -debug: false tasks: - name: "johndoe" from: @@ -46,6 +45,7 @@ tasks: Usage of rover: -config string path to the config file (default "rover.yml") + -verbose ``` ## Legal diff --git a/cmd/rover/main.go b/cmd/rover/main.go index d262f8d..fea5152 100644 --- a/cmd/rover/main.go +++ b/cmd/rover/main.go @@ -1,162 +1,34 @@ package main import ( - "bytes" - "encoding/json" + "context" "flag" - "fmt" - "io" "log" - "net/http" - "os" + "os/signal" + "syscall" - "github.com/emersion/go-imap" - "github.com/emersion/go-imap/client" - "github.com/emersion/go-message/mail" + "github.com/ruhrcloud/rover/internal/config" + "github.com/ruhrcloud/rover/internal/tasks" ) -type Imap struct { - Host string `json:"host"` - User string `json:"user"` - Pass string `json:"pass"` -} - -type Webdav struct { - Baseurl string `json:"baseurl"` - User string `json:"user"` - Pass string `json:"pass"` -} - -type Config struct { - Imap Imap `json:"imap"` - Webdav Webdav `json:"webdav"` -} - func main() { - var file string - flag.StringVar(&file, "config", "config.json", "path to the config file") + var cfgPath string + var verbose bool + flag.StringVar(&cfgPath, "config", "rover.yml", "config file") + flag.BoolVar(&verbose, "verbose", false, "verbose") flag.Parse() - f, err := os.Open(file) - if err != nil { - log.Fatal("Error opening config:", err) - } - defer f.Close() - - byt, err := io.ReadAll(f) - if err != nil { - log.Fatal("Error reading config:", err) - } - - var conf Config - json.Unmarshal(byt, &conf) - - client, err := client.DialTLS(conf.Imap.Host, nil) - if err != nil { - log.Fatal("Error connecting to IMAP server:", err) - } - defer client.Logout() - - if err := client.Login(conf.Imap.User, conf.Imap.Pass); err != nil { - log.Fatal("IMAP login failed:", err) - } - log.Println("Logged in to IMAP server") - - mbox, err := client.Select("INBOX", false) + cfg, err := config.Load(cfgPath) if err != nil { - log.Fatal("Unable to select INBOX:", err) + log.Fatal(err) } - log.Printf("Mailbox %s selected. Total messages: %d\n", mbox.Name, mbox.Messages) - criteria := imap.NewSearchCriteria() - criteria.WithoutFlags = []string{"\\Seen"} - uids, err := client.Search(criteria) + ctx, stop := signal.NotifyContext(context.Background(), + syscall.SIGINT, syscall.SIGTERM) + defer stop() + err = tasks.Run(ctx, cfg) if err != nil { - log.Fatal("Search error:", err) + log.Fatal(err) } - if len(uids) == 0 { - log.Println("No unread messages found.") - return - } - log.Println("Found message UIDs:", uids) - - seqset := new(imap.SeqSet) - seqset.AddNum(uids...) - section := &imap.BodySectionName{} - messages := make(chan *imap.Message, 10) - done := make(chan error, 1) - - go func() { - done <- client.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope, section.FetchItem()}, messages) - }() - - for msg := range messages { - if msg.Envelope == nil { - continue - } - log.Printf("Processing message UID %d: %s\n", msg.Uid, msg.Envelope.Subject) - - r := msg.GetBody(section) - if r == nil { - log.Println("No message body returned for UID", msg.Uid) - continue - } - - mr, err := mail.CreateReader(r) - if err != nil { - log.Println("Failed to create mail reader for UID", msg.Uid, ":", err) - continue - } - - for { - part, err := mr.NextPart() - if err == io.EOF { - break - } - if err != nil { - log.Println("Error reading part:", err) - break - } - - if h, ok := part.Header.(*mail.AttachmentHeader); ok { - filename, _ := h.Filename() - log.Println("Found attachment:", filename) - - buf := new(bytes.Buffer) - if _, err := io.Copy(buf, part.Body); err != nil { - log.Println("Error reading attachment:", err) - continue - } - - uploadURL := fmt.Sprintf("%s%s", conf.Webdav.Baseurl, filename) - log.Println("Uploading to:", uploadURL) - - req, err := http.NewRequest("PUT", uploadURL, bytes.NewReader(buf.Bytes())) - if err != nil { - log.Println("Error creating HTTP request:", err) - continue - } - req.SetBasicAuth(conf.Webdav.User, conf.Webdav.Pass) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - log.Println("Error uploading attachment:", err) - continue - } - resp.Body.Close() - - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - log.Printf("Attachment %s uploaded successfully.\n", filename) - } else { - log.Printf("Failed to upload %s: %s\n", filename, resp.Status) - } - } - } - } - - if err := <-done; err != nil { - log.Fatal("Fetch error:", err) - } - - log.Println("Processing complete.") } + diff --git a/go.mod b/go.mod index 6e41c9b..4634002 100644 --- a/go.mod +++ b/go.mod @@ -10,5 +10,9 @@ require ( require ( github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect + github.com/emersion/go-webdav v0.6.0 // indirect + github.com/gosimple/slug v1.15.0 // indirect + github.com/gosimple/unidecode v1.0.1 // indirect + golang.org/x/net v0.43.0 // indirect golang.org/x/text v0.28.0 // indirect ) diff --git a/go.sum b/go.sum index 41f5cfb..2b99513 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw= github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA= github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= @@ -7,6 +8,14 @@ github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTe github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= +github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= +github.com/emersion/go-webdav v0.6.0 h1:rbnBUEXvUM2Zk65Him13LwJOBY0ISltgqM5k6T5Lq4w= +github.com/emersion/go-webdav v0.6.0/go.mod h1:mI8iBx3RAODwX7PJJ7qzsKAKs/vY429YfS2/9wKnDbQ= +github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= +github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= +github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= +github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -16,6 +25,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..5793e6c --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,100 @@ +package config + +import ( + "errors" + "os" + "strings" + "time" + + "gopkg.in/yaml.v3" +) + +type FromConf struct { + Host string `yaml:"host"` + User string `yaml:"user"` + Pass string `yaml:"pass"` + Mailbox string `yaml:"mailbox"` +} + +type ToConf struct { + BaseURL string `yaml:"base_url"` + User string `yaml:"user"` + Pass string `yaml:"pass"` + Auth string `yaml:"auth"` + Token string `yaml:"token"` +} + +type FilterConf struct { + Recipients []string `yaml:"recipients"` + Seen *bool `yaml:"seen"` + Extensions []string `yaml:"extensions"` +} + +type Task struct { + Name string `yaml:"name"` + From FromConf `yaml:"from"` + To ToConf `yaml:"to"` + Path string `yaml:"path"` + Tags []string `yaml:"tags"` + Filter FilterConf `yaml:"filter"` + Interval string `yaml:"interval"` + Format string `yaml:"format"` + MarkSeen bool `yaml:"mark_seen"` +} + +type Config struct { + Tasks []Task `yaml:"tasks"` +} + +func Load(p string) (*Config, error) { + b, err := os.ReadFile(p) + if err != nil { + return nil, err + } + var c Config + if err := yaml.Unmarshal(b, &c); err != nil { + return nil, err + } + for i := range c.Tasks { + if err := validateTask(c.Tasks[i]); err != nil { + return nil, err + } + } + return &c, nil +} + +func validateTask(t Task) error { + if strings.TrimSpace(t.Name) == "" { + return errors.New("task.name required") + } + + if t.From.User == "" || t.From.Pass == "" { + return errors.New("from.user/from.pass required") + } + if t.From.Mailbox == "" { + return errors.New("from.mailbox required") + } + if t.To.BaseURL == "" { + return errors.New("to.base_url required") + } + switch strings.ToLower(strings.TrimSpace(t.To.Auth)) { + case "", "basic": + if t.To.User == "" || t.To.Pass == "" { + return errors.New("to.user/to.pass required for basic auth") + } + case "bearer": + if t.To.Token == "" { + return errors.New("to.token required for bearer auth") + } + default: + return errors.New("to.auth must be basic or bearer") + } + if _, err := time.ParseDuration(t.Interval); err != nil { + return errors.New("interval must be a valid duration") + } + if strings.TrimSpace(t.Format) == "" { + return errors.New("format template required") + } + return nil +} + diff --git a/internal/email/email.go b/internal/email/email.go new file mode 100644 index 0000000..31a760f --- /dev/null +++ b/internal/email/email.go @@ -0,0 +1,302 @@ +package email + +import ( + "bytes" + "context" + "crypto/tls" + "errors" + "fmt" + "io" + "log" + "mime" + "path" + "strings" + "text/template" + "time" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/client" + message "github.com/emersion/go-message" + "github.com/emersion/go-message/mail" + "github.com/gosimple/slug" + "golang.org/x/net/html/charset" + + "github.com/ruhrcloud/rover/internal/config" + webdav "github.com/ruhrcloud/rover/internal/webdav" +) + +type Result struct { + Processed int + SkippedRecipient int + MsgsNoAttachments int + MsgsWithAttachments int + TotalParts int + Uploaded int + SeenToMark []uint32 +} + +func init() { + message.CharsetReader = charset.NewReaderLabel +} + +func Once(ctx context.Context, t config.Task, dav *webdav.Client) (Result, error) { + var res Result + + opts := t.From + conn, err := client.Dial(opts.Host) + if err != nil { + return res, err + } + defer conn.Logout() + + tlsConfig := &tls.Config{ServerName: "mail.your-server.de"} + err = conn.StartTLS(tlsConfig) + if err != nil { + return res, err + } + + err = conn.Login(opts.User, opts.Pass) + if err != nil { + return res, err + } + + _, err = conn.Select(opts.Mailbox, false) + if err != nil { + return res, err + } + + crit := imap.NewSearchCriteria() + if t.Filter.Seen != nil { + if *t.Filter.Seen { + crit.WithFlags = []string{imap.SeenFlag} + } else { + crit.WithoutFlags = []string{imap.SeenFlag} + } + } + + uids, err := conn.UidSearch(crit) + if err != nil { + return res, err + } + if len(uids) == 0 { + log.Printf("[%s] no messages matched criteria", t.Name) + return res, nil + } + + seq := new(imap.SeqSet) + seq.AddNum(uids...) + section := &imap.BodySectionName{} + items := []imap.FetchItem{ + imap.FetchEnvelope, + imap.FetchUid, + section.FetchItem(), + } + msgCh := make(chan *imap.Message, 16) + done := make(chan error, 1) + go func() { + done <- conn.UidFetch(seq, items, msgCh) + }() + + tmpl := template.New("name").Funcs( + template.FuncMap{ + "slug": slug.Make, + }) + _, err = tmpl.Parse(t.Format) + if err != nil { + return res, fmt.Errorf("failed to parse format %w", err) + } + + parts := []string{} + if v := strings.Trim(t.Path, "/"); v != "" { + parts = append(parts, v) + } + for _, tg := range t.Tags { + if v := strings.Trim(strings.TrimSpace(tg), "/"); v != "" { + parts = append(parts, v) + } + } + relDir := strings.Trim(strings.Join(parts, "/"), "/") + if relDir != "" { + if err := dav.Mkdir(ctx, relDir); err != nil { + return res, fmt.Errorf("mkdir %q: %w", "/"+relDir, err) + } + } + + allowed := map[string]struct{}{} + for _, e := range t.Filter.Extensions { + e = strings.ToLower(strings.TrimPrefix(strings.TrimSpace(e), ".")) + if e != "" { + allowed[e] = struct{}{} + } + } + byExt := len(allowed) > 0 + dec := new(mime.WordDecoder) + + for msg := range msgCh { + if msg == nil || msg.Envelope == nil { + continue + } + en := msg.Envelope + if len(t.Filter.Recipients) > 0 && !recipientsMatch(en, t.Filter.Recipients) { + // log.Printf("[%s] UID %d: skip (recipient filter)", t.Name, msg.Uid) + res.SkippedRecipient++ + continue + } + r := msg.GetBody(section) + if r == nil { + res.Processed++ + continue + } + mr, err := mail.CreateReader(r) + if err != nil { + res.Processed++ + continue + } + subj := en.Subject + if s, err := dec.DecodeHeader(subj); err == nil && s != "" { + subj = s + } + when := en.Date + if when.IsZero() { + when = time.Now() + } + + att := 0 + up := 0 + + for { + p, err := mr.NextPart() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + break + } + ah, ok := p.Header.(*mail.AttachmentHeader) + if !ok { + continue + } + name, _ := ah.Filename() + if name == "" { + name = "attachment.bin" + } + ext := strings.TrimPrefix(strings.ToLower(path.Ext(name)), ".") + if byExt { + if _, ok := allowed[ext]; !ok { + // log.Printf("[%s] UID %d: skip attachment %q by extension", t.Name, msg.Uid, name) + continue + } + } + att++ + base := strings.TrimSuffix(name, "."+ext) + + buf := new(bytes.Buffer) + if _, err := io.Copy(buf, p.Body); err != nil { + continue + } + + data := struct { + OrigBase string + OrigExt string + Subject string + UID uint32 + Date string + DateTime string + }{ + OrigBase: base, + OrigExt: "." + ext, + Subject: subj, + UID: msg.Uid, + Date: when.Format("2006-01-02"), + DateTime: when.Format("20060102-150405"), + } + var b bytes.Buffer + if err := tmpl.Execute(&b, data); err != nil || b.Len() == 0 { + return res, fmt.Errorf("template execution failed") + } + + out := strings.TrimSpace(b.String()) + out = strings.ReplaceAll(out, "/", "-") + out = strings.ReplaceAll(out, "\\", "-") + if path.Ext(out) == "" && ext != "" { + out += "." + ext + } + target := out + if relDir != "" { + target = webdav.Join(relDir, out) + } + if exists, _ := dav.Exists(ctx, target); exists { + dir := path.Dir(target) + bn := path.Base(target) + be := strings.TrimSuffix(bn, path.Ext(bn)) + target = webdav.Join(dir, be+"-dup."+ext) + } + if err := dav.Create(ctx, target, buf.Bytes()); err != nil { + log.Printf("[%s] UID %d: upload /%s failed: %v", t.Name, msg.Uid, target, err) + continue + } + log.Printf("[%s] uploaded -> /%s", t.Name, target) + up++ + res.Uploaded++ + } + + if att == 0 { + res.MsgsNoAttachments++ + } else { + res.MsgsWithAttachments++ + res.TotalParts += att + } + if up > 0 && t.MarkSeen { + res.SeenToMark = append(res.SeenToMark, msg.Uid) + } + res.Processed++ + } + if err := <-done; err != nil { + return res, err + } + return res, nil +} + +func MarkSeen(ctx context.Context, t config.Task, uids []uint32) error { + if len(uids) == 0 { + return nil + } + cli, err := client.DialTLS(t.From.Host, &tls.Config{ServerName: strings.Split(t.From.Host, ":")[0]}) + if err != nil { + return err + } + defer cli.Logout() + if err := cli.Login(t.From.User, t.From.Pass); err != nil { + return err + } + if _, err := cli.Select(t.From.Mailbox, false); err != nil { + return err + } + seq := new(imap.SeqSet) + seq.AddNum(uids...) + item := imap.FormatFlagsOp(imap.AddFlags, true) + flags := []interface{}{imap.SeenFlag} + return cli.UidStore(seq, item, flags, nil) +} + +func recipientsMatch(env *imap.Envelope, want []string) bool { + m := map[string]struct{}{} + for _, w := range want { + if s := strings.ToLower(strings.TrimSpace(w)); s != "" { + m[s] = struct{}{} + } + } + check := func(l []*imap.Address) bool { + for _, a := range l { + if a == nil { + continue + } + if _, ok := m[strings.ToLower(strings.TrimSpace(a.Address()))]; ok { + return true + } + } + return false + } + return check(env.To) || check(env.Cc) || check(env.Bcc) +} + diff --git a/internal/tasks/tasks.go b/internal/tasks/tasks.go new file mode 100644 index 0000000..aea1724 --- /dev/null +++ b/internal/tasks/tasks.go @@ -0,0 +1,70 @@ +package tasks + +import ( + "context" + "log" + "time" + + "github.com/ruhrcloud/rover/internal/config" + "github.com/ruhrcloud/rover/internal/email" + webdav "github.com/ruhrcloud/rover/internal/webdav" +) + +func Run(ctx context.Context, cfg *config.Config) error { + for i := range cfg.Tasks { + t := cfg.Tasks[i] + go loop(ctx, t) + } + + <-ctx.Done() + return nil +} + +func loop(ctx context.Context, t config.Task) { + duration, err := time.ParseDuration(t.Interval) + if err != nil { + duration = time.Duration(5 * float64(time.Minute)) + } + log.Printf("[%s] starting task to run every %s", t.Name, duration) + + ticker := time.NewTicker(duration) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + opts := t.To + client, err := webdav.New(webdav.Opts{ + BaseURL: opts.BaseURL, + Auth: opts.Auth, + User: opts.User, + Pass: opts.Pass, + Token: opts.Token, + }) + if err != nil { + log.Printf("[%s] webdav: %v", t.Name, err) + continue + } + + res, err := email.Once(ctx, t, client) + if err != nil { + log.Printf("[%s] %v", t.Name, err) + continue + } + + o := len(res.SeenToMark) > 0 + if o && t.MarkSeen { + err := email.MarkSeen(ctx, t, res.SeenToMark) + if err != nil { + log.Printf("[%s] failed to mark seen: %v", t.Name, err) + } + } + + log.Printf("[%s] processed %d and uploaded %d attachments", + t.Name, res.Processed, res.Uploaded) + } + } +} + diff --git a/internal/webdav/client.go b/internal/webdav/client.go new file mode 100644 index 0000000..3688ac5 --- /dev/null +++ b/internal/webdav/client.go @@ -0,0 +1,106 @@ +package webdav + +import ( + "context" + "fmt" + "net/http" + "strings" + + gowebdav "github.com/emersion/go-webdav" +) + +type Client struct{ c *gowebdav.Client } + +type Opts struct { + BaseURL string + Auth string + User string + Pass string + Token string +} + +type bearer struct { + base gowebdav.HTTPClient + tok string +} + +func (b bearer) Do(r *http.Request) (*http.Response, error) { + if b.base == nil { + b.base = http.DefaultClient + } + cl := r.Clone(r.Context()) + cl.Header.Set("Authorization", "Bearer "+b.tok) + return b.base.Do(cl) +} + +func New(o Opts) (*Client, error) { + endpoint := strings.TrimSpace(o.BaseURL) + if endpoint == "" { + return nil, fmt.Errorf("empty base_url") + } + var hc gowebdav.HTTPClient + switch strings.ToLower(strings.TrimSpace(o.Auth)) { + case "", "basic": + hc = gowebdav.HTTPClientWithBasicAuth(nil, o.User, o.Pass) + case "bearer": + hc = bearer{tok: o.Token} + default: + return nil, fmt.Errorf("unsupported auth") + } + c, err := gowebdav.NewClient(hc, endpoint) + if err != nil { + return nil, err + } + return &Client{c: c}, nil +} + +func (c *Client) Mkdir(ctx context.Context, relDir string) error { + relDir = strings.Trim(relDir, "/") + if relDir == "" { + return nil + } + cur := "" + for _, seg := range strings.Split(relDir, "/") { + if seg == "" { + continue + } + cur = strings.Trim(strings.Join([]string{cur, seg}, "/"), "/") + if err := c.c.Mkdir(ctx, cur); err != nil { + if fi, e := c.c.Stat(ctx, cur); e == nil && fi.IsDir { + continue + } + return err + } + } + return nil +} + +func (c *Client) Create(ctx context.Context, relPath string, data []byte) error { + w, err := c.c.Create(ctx, strings.Trim(relPath, "/")) + if err != nil { + return err + } + if _, err := w.Write(data); err != nil { + _ = w.Close() + return err + } + return w.Close() +} + +func (c *Client) Exists(ctx context.Context, relPath string) (bool, error) { + _, err := c.c.Stat(ctx, strings.Trim(relPath, "/")) + if err != nil { + return false, err + } + return true, nil +} + +func Join(dir, file string) string { + dir = strings.Trim(dir, "/") + file = strings.Trim(file, "/") + if dir == "" { + return file + } + return dir + "/" + file +} + diff --git a/rover.yml b/rover.yml index 57f146c..8945745 100644 --- a/rover.yml +++ b/rover.yml @@ -1,4 +1,3 @@ -debug: false tasks: - name: "johndoe" from: