2020-03-27 11:01:41 +01:00
|
|
|
/*
|
2020-06-23 13:44:14 +02:00
|
|
|
|
|
|
|
Package webdav implements sharing with WebDAV servers.
|
|
|
|
|
2021-01-01 22:37:25 +01:00
|
|
|
Copyright (c) 2018 - 2021 Michael Mayer <hello@photoprism.org>
|
2020-06-23 13:44:14 +02:00
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU Affero General Public License as published
|
|
|
|
by the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
2020-09-21 02:48:22 +02:00
|
|
|
PhotoPrism® is a registered trademark of Michael Mayer. You may use it as required
|
2020-06-23 13:53:11 +02:00
|
|
|
to describe our software, run your own server, for educational purposes, but not for
|
|
|
|
offering commercial goods, products, or services without prior written permission.
|
|
|
|
In other words, please ask.
|
2020-06-23 13:44:14 +02:00
|
|
|
|
|
|
|
Feel free to send an e-mail to hello@photoprism.org if you have questions,
|
|
|
|
want to support our work, or just want to say hello.
|
2020-03-27 11:01:41 +01:00
|
|
|
|
|
|
|
Additional information can be found in our Developer Guide:
|
2020-06-23 13:44:14 +02:00
|
|
|
https://docs.photoprism.org/developer-guide/
|
2020-03-27 11:01:41 +01:00
|
|
|
|
|
|
|
*/
|
|
|
|
package webdav
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"path"
|
2020-04-24 14:11:17 +02:00
|
|
|
"time"
|
2020-03-27 11:01:41 +01:00
|
|
|
|
|
|
|
"github.com/photoprism/photoprism/internal/event"
|
2020-04-02 18:17:07 +02:00
|
|
|
"github.com/photoprism/photoprism/pkg/fs"
|
2020-03-27 11:01:41 +01:00
|
|
|
"github.com/studio-b12/gowebdav"
|
|
|
|
)
|
|
|
|
|
|
|
|
var log = event.Log
|
|
|
|
|
2020-12-05 01:24:33 +01:00
|
|
|
const SyncTimeout = time.Second * 45
|
|
|
|
const AsyncTimeout = time.Minute * 20
|
|
|
|
|
2020-03-27 11:01:41 +01:00
|
|
|
type Client struct {
|
|
|
|
client *gowebdav.Client
|
|
|
|
}
|
|
|
|
|
2020-04-03 18:08:49 +02:00
|
|
|
// New creates a new WebDAV client.
|
|
|
|
func New(url, user, pass string) Client {
|
2020-03-27 11:01:41 +01:00
|
|
|
clt := gowebdav.NewClient(url, user, pass)
|
|
|
|
|
2020-04-24 14:11:17 +02:00
|
|
|
clt.SetTimeout(10 * time.Minute) // TODO: Change timeout if needed
|
|
|
|
|
2020-12-05 01:24:33 +01:00
|
|
|
result := Client{
|
|
|
|
client: clt,
|
|
|
|
}
|
2020-03-27 11:01:41 +01:00
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2020-03-27 11:56:24 +01:00
|
|
|
func (c Client) readDir(path string) ([]os.FileInfo, error) {
|
|
|
|
if path == "" {
|
|
|
|
path = "/"
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.client.ReadDir(path)
|
|
|
|
}
|
|
|
|
|
2020-03-27 11:01:41 +01:00
|
|
|
// Files returns all files in path as string slice.
|
2020-04-02 18:17:07 +02:00
|
|
|
func (c Client) Files(dir string) (result fs.FileInfos, err error) {
|
2020-03-31 17:26:25 +02:00
|
|
|
files, err := c.readDir(dir)
|
2020-03-27 11:01:41 +01:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return result, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, file := range files {
|
2020-03-27 18:17:07 +01:00
|
|
|
if !file.Mode().IsRegular() {
|
|
|
|
continue
|
|
|
|
}
|
2020-04-02 18:17:07 +02:00
|
|
|
|
|
|
|
info := fs.NewFileInfo(file, dir)
|
|
|
|
|
|
|
|
result = append(result, info)
|
2020-03-27 11:01:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Directories returns all sub directories in path as string slice.
|
2020-12-05 01:24:33 +01:00
|
|
|
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) {
|
2020-03-31 17:26:25 +02:00
|
|
|
files, err := c.readDir(root)
|
2020-03-27 11:01:41 +01:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return result, err
|
|
|
|
}
|
|
|
|
|
2020-03-31 17:26:25 +02:00
|
|
|
if root == "/" {
|
|
|
|
root = ""
|
|
|
|
}
|
|
|
|
|
2020-03-27 11:01:41 +01:00
|
|
|
for _, file := range files {
|
2020-03-27 18:17:07 +01:00
|
|
|
if !file.Mode().IsDir() {
|
|
|
|
continue
|
|
|
|
}
|
2020-03-27 11:56:24 +01:00
|
|
|
|
2020-04-02 18:17:07 +02:00
|
|
|
info := fs.NewFileInfo(file, root)
|
2020-03-31 17:26:25 +02:00
|
|
|
|
2020-04-02 18:17:07 +02:00
|
|
|
result = append(result, info)
|
2020-03-27 11:56:24 +01:00
|
|
|
|
2020-12-05 01:24:33 +01:00
|
|
|
if recursive && time.Now().Sub(start) < timeout {
|
|
|
|
subDirs, err := c.fetchDirs(info.Abs, true, start, timeout)
|
2020-03-27 11:56:24 +01:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return result, err
|
|
|
|
}
|
|
|
|
|
|
|
|
result = append(result, subDirs...)
|
|
|
|
}
|
2020-03-27 11:01:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Download downloads a single file to the given location.
|
2020-04-06 16:34:29 +02:00
|
|
|
func (c Client) Download(from, to string, force bool) error {
|
|
|
|
if _, err := os.Stat(to); err == nil && !force {
|
|
|
|
return fmt.Errorf("webdav: download skipped, %s already exists", to)
|
|
|
|
}
|
|
|
|
|
2020-03-27 11:01:41 +01:00
|
|
|
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: can't create %s (%s)", dir, err)
|
|
|
|
}
|
|
|
|
} else if !dirInfo.IsDir() {
|
2020-05-07 12:33:09 +02:00
|
|
|
return fmt.Errorf("webdav: %s is not a folder", dir)
|
2020-03-27 11:01:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
bytes, err := c.client.Read(from)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-10-06 07:10:50 +02:00
|
|
|
return os.WriteFile(to, bytes, 0644)
|
2020-03-27 11:01:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// DownloadDir downloads all files from a remote to a local directory.
|
2020-04-06 16:34:29 +02:00
|
|
|
func (c Client) DownloadDir(from, to string, recursive, force bool) (errs []error) {
|
2020-03-27 11:01:41 +01:00
|
|
|
files, err := c.Files(from)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return append(errs, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, file := range files {
|
2020-04-02 18:17:07 +02:00
|
|
|
dest := to + string(os.PathSeparator) + file.Abs
|
2020-03-27 11:01:41 +01:00
|
|
|
|
|
|
|
if _, err := os.Stat(dest); err == nil {
|
|
|
|
// File exists
|
|
|
|
msg := fmt.Errorf("webdav: %s exists", dest)
|
|
|
|
errs = append(errs, msg)
|
|
|
|
log.Error(msg)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-04-06 16:34:29 +02:00
|
|
|
if err := c.Download(file.Abs, dest, force); err != nil {
|
2020-03-27 11:01:41 +01:00
|
|
|
msg := fmt.Errorf("webdav: %s", err)
|
|
|
|
errs = append(errs, msg)
|
|
|
|
log.Error(msg)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !recursive {
|
|
|
|
return errs
|
|
|
|
}
|
|
|
|
|
2020-12-05 01:24:33 +01:00
|
|
|
dirs, err := c.Directories(from, false, AsyncTimeout)
|
2020-03-27 11:01:41 +01:00
|
|
|
|
|
|
|
for _, dir := range dirs {
|
2020-04-06 16:34:29 +02:00
|
|
|
errs = append(errs, c.DownloadDir(dir.Abs, to, true, force)...)
|
2020-03-27 11:01:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return errs
|
|
|
|
}
|
|
|
|
|
2020-04-01 12:00:45 +02:00
|
|
|
// CreateDir recursively creates directories if they don't exist.
|
|
|
|
func (c Client) CreateDir(dir string) error {
|
2020-04-07 12:51:01 +02:00
|
|
|
if dir == "" || dir == "/" || dir == "." {
|
2020-04-01 12:00:45 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.client.MkdirAll(dir, os.ModePerm)
|
|
|
|
}
|
|
|
|
|
2020-03-27 11:01:41 +01:00
|
|
|
// Upload uploads a single file to the remote server.
|
|
|
|
func (c Client) Upload(from, to string) error {
|
|
|
|
file, err := os.Open(from)
|
|
|
|
|
2020-05-28 21:20:42 +02:00
|
|
|
if err != nil || file == nil {
|
2020-03-27 11:01:41 +01:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|