/* Package webdav implements sharing with WebDAV servers. Copyright (c) 2018 - 2022 Michael Mayer This program is free software: you can redistribute it and/or modify it under Version 3 of the GNU Affero General Public License (the "AGPL"): This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. The AGPL is supplemented by our Trademark and Brand Guidelines, which describe how our Brand Assets may be used: Feel free to send an e-mail to hello@photoprism.app if you have questions, want to support our work, or just want to say hello. Additional information can be found in our Developer Guide: */ package webdav import ( "fmt" "os" "path" "runtime/debug" "time" "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/pkg/fs" "github.com/studio-b12/gowebdav" ) var log = event.Log const SyncTimeout = time.Second * 45 const AsyncTimeout = time.Minute * 20 type Client struct { client *gowebdav.Client } // New creates a new WebDAV client. func New(url, user, pass string) Client { clt := gowebdav.NewClient(url, user, pass) clt.SetTimeout(10 * time.Minute) // TODO: Change timeout if needed result := Client{ client: clt, } return result } func (c Client) readDir(path string) ([]os.FileInfo, error) { if path == "" { path = "/" } return c.client.ReadDir(path) } // Files returns all files in a directory as string slice. func (c Client) Files(dir string) (result fs.FileInfos, err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("webdav: %s (panic while listing files)\nstack: %s", r, debug.Stack()) } }() files, err := c.readDir(dir) if err != nil { return result, err } for _, file := range files { if !file.Mode().IsRegular() { continue } info := fs.NewFileInfo(file, dir) result = append(result, info) } return result, nil } // Directories returns all sub directories in path as string slice. func (c Client) Directories(root string, recursive bool, timeout time.Duration) (result fs.FileInfos, err error) { start := time.Now() result, err = c.fetchDirs(root, recursive, start, timeout) if time.Now().Sub(start) >= timeout { log.Warnf("webdav: read dir timeout reached") } return result, err } // fetchDirs recursively fetches all directories until the timeout is reached. func (c Client) fetchDirs(root string, recursive bool, start time.Time, timeout time.Duration) (result fs.FileInfos, err error) { files, err := c.readDir(root) if err != nil { return result, err } if root == "/" { root = "" } for _, file := range files { if !file.Mode().IsDir() { continue } info := fs.NewFileInfo(file, root) result = append(result, info) if recursive && time.Now().Sub(start) < timeout { subDirs, err := c.fetchDirs(info.Abs, true, start, timeout) if err != nil { return result, err } result = append(result, subDirs...) } } return result, nil } // Download downloads a single file to the given location. func (c Client) Download(from, to string, force bool) (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("webdav: %s (panic while downloading)\nstack: %s", r, debug.Stack()) } }() if _, err := os.Stat(to); err == nil && !force { return fmt.Errorf("webdav: download skipped, %s already exists", to) } dir := path.Dir(to) dirInfo, err := os.Stat(dir) if err != nil { // Create directory if err := os.MkdirAll(dir, os.ModePerm); err != nil { return fmt.Errorf("webdav: cannot create %s (%s)", dir, err) } } else if !dirInfo.IsDir() { return fmt.Errorf("webdav: %s is not a folder", dir) } var bytes []byte bytes, err = c.client.Read(from) if err != nil { return err } return os.WriteFile(to, bytes, 0644) } // DownloadDir downloads all files from a remote to a local directory. func (c Client) DownloadDir(from, to string, recursive, force bool) (errs []error) { files, err := c.Files(from) if err != nil { return append(errs, err) } for _, file := range files { dest := to + string(os.PathSeparator) + file.Abs if _, err = os.Stat(dest); err == nil { // File already exists. msg := fmt.Errorf("webdav: %s exists", dest) errs = append(errs, msg) log.Warn(msg) continue } if err = c.Download(file.Abs, dest, force); err != nil { // Failed to download file. errs = append(errs, err) log.Error(err) continue } } if !recursive { return errs } dirs, err := c.Directories(from, false, AsyncTimeout) for _, dir := range dirs { errs = append(errs, c.DownloadDir(dir.Abs, to, true, force)...) } return errs } // CreateDir recursively creates directories if they don't exist. func (c Client) CreateDir(dir string) error { if dir == "" || dir == "/" || dir == "." { return nil } return c.client.MkdirAll(dir, os.ModePerm) } // Upload uploads a single file to the remote server. func (c Client) Upload(from, to string) (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("webdav: %s (panic while uploading)\nstack: %s", r, debug.Stack()) } }() file, err := os.Open(from) if err != nil || file == nil { return err } defer func(file *os.File) { _ = file.Close() }(file) return c.client.WriteStream(to, file, 0644) } // Delete deletes a single file or directory on a remote server. func (c Client) Delete(path string) error { return c.client.Remove(path) }