diff --git a/Makefile b/Makefile index fb69cce4a..6b2862d41 100644 --- a/Makefile +++ b/Makefile @@ -132,6 +132,9 @@ docker-tensorflow: docker-tidb: scripts/docker-build.sh tidb $(TIDB_VERSION) scripts/docker-push.sh tidb $(TIDB_VERSION) +docker-webdav: + scripts/docker-build.sh webdav $(DOCKER_TAG) + scripts/docker-push.sh webdav $(DOCKER_TAG) lint-js: (cd frontend && npm run lint) fmt-js: diff --git a/docker-compose.travis.yml b/docker-compose.travis.yml index f30a3a219..aac32425b 100644 --- a/docker-compose.travis.yml +++ b/docker-compose.travis.yml @@ -50,3 +50,6 @@ services: MYSQL_USER: photoprism MYSQL_PASSWORD: photoprism MYSQL_DATABASE: photoprism + + webdav-dummy: + image: photoprism/webdav:20200327 diff --git a/docker-compose.yml b/docker-compose.yml index 82c75ceed..c84e64d5b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -53,3 +53,6 @@ services: MYSQL_USER: photoprism MYSQL_PASSWORD: photoprism MYSQL_DATABASE: photoprism + + webdav-dummy: + image: photoprism/webdav:20200327 diff --git a/docker/webdav/Dockerfile b/docker/webdav/Dockerfile new file mode 100644 index 000000000..39181b154 --- /dev/null +++ b/docker/webdav/Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.14 + +LABEL maintainer="Michael Mayer " + +# Set up project directory +WORKDIR "/webdav" + +RUN go get github.com/hacdias/webdav + +# Expose HTTP port +EXPOSE 80 + +COPY /docker/webdav/config.yml /webdav/config.yml +COPY /docker/webdav/files /webdav/files + +CMD webdav -c /webdav/config.yml diff --git a/docker/webdav/config.yml b/docker/webdav/config.yml new file mode 100644 index 000000000..a86c99859 --- /dev/null +++ b/docker/webdav/config.yml @@ -0,0 +1,16 @@ +# Server related settings +address: 0.0.0.0 +port: 80 +auth: true +tls: false +prefix: / + +# Default settings +scope: /webdav/files +modify: true +rules: [] + +# Accounts +users: + - username: admin + password: photoprism diff --git a/docker/webdav/files/Photos/2020/03/iphone_7.heic b/docker/webdav/files/Photos/2020/03/iphone_7.heic new file mode 100644 index 000000000..4a8f75f9d Binary files /dev/null and b/docker/webdav/files/Photos/2020/03/iphone_7.heic differ diff --git a/docker/webdav/files/Photos/2020/03/iphone_7.json b/docker/webdav/files/Photos/2020/03/iphone_7.json new file mode 100644 index 000000000..1e24fcf19 --- /dev/null +++ b/docker/webdav/files/Photos/2020/03/iphone_7.json @@ -0,0 +1,139 @@ +[{ + "SourceFile": "IMG_3963.HEIC", + "ExifTool": { + "ExifToolVersion": 10.80 + }, + "File": { + "FileName": "IMG_3963.HEIC", + "Directory": ".", + "FileSize": "767 kB", + "FileModifyDate": "2019:06:06 03:46:59+00:00", + "FileAccessDate": "2019:06:06 03:49:46+00:00", + "FileInodeChangeDate": "2019:06:06 03:47:56+00:00", + "FilePermissions": "rw-r--r--", + "FileType": "HEIC", + "FileTypeExtension": "heic", + "MIMEType": "image/heic", + "ExifByteOrder": "Big-endian (Motorola, MM)", + "ImageWidth": 4032, + "ImageHeight": 3024 + }, + "QuickTime": { + "MajorBrand": "High Efficiency Image Format HEVC still image (.HEIC)", + "MinorVersion": "0.0.0", + "CompatibleBrands": ["mif1","heic"], + "HandlerType": "Picture", + "PrimaryItemReference": 49, + "ImageSpatialExtent": "4032x3024", + "ImagePixelDepth": "8 8 8", + "MovieDataSize": 782453, + "MovieDataOffset": 3290 + }, + "EXIF": { + "Make": "Apple", + "Model": "iPhone 7", + "Orientation": "Rotate 90 CW", + "XResolution": 72, + "YResolution": 72, + "ResolutionUnit": "inches", + "Software": "ProCam 10.5.8", + "ModifyDate": "2018:09:10 12:16:13", + "ExposureTime": "1/4000", + "FNumber": 1.8, + "ExposureProgram": "Program AE", + "ISO": 20, + "ExifVersion": "0221", + "DateTimeOriginal": "2018:09:10 12:16:13", + "CreateDate": "2018:09:10 12:16:13", + "ComponentsConfiguration": "Y, Cb, Cr, -", + "ShutterSpeedValue": "1/4000", + "ApertureValue": 1.8, + "BrightnessValue": 11.39478764, + "ExposureCompensation": 0, + "MeteringMode": "Multi-segment", + "Flash": "Off, Did not fire", + "FocalLength": "4.0 mm", + "SubjectArea": "2009 1510 2213 1331", + "SubSecTimeOriginal": "023", + "SubSecTimeDigitized": "023", + "FlashpixVersion": "0100", + "ColorSpace": "Uncalibrated", + "ExifImageWidth": 4032, + "ExifImageHeight": 3024, + "SensingMethod": "One-chip color area", + "SceneType": "Directly photographed", + "ExposureMode": "Auto", + "WhiteBalance": "Auto", + "DigitalZoomRatio": 1, + "FocalLengthIn35mmFormat": "74 mm", + "SceneCaptureType": "Standard", + "LensInfo": "3.99mm f/1.8", + "LensMake": "Apple", + "LensModel": "iPhone 7 back camera 3.99mm f/1.8", + "GPSLatitudeRef": "North", + "GPSLatitude": "34 deg 47' 50.82\"", + "GPSLongitudeRef": "East", + "GPSLongitude": "134 deg 45' 52.68\"" + }, + "MakerNotes": { + "RunTimeFlags": "Valid", + "RunTimeValue": 67281333929291, + "RunTimeScale": 1000000000, + "RunTimeEpoch": 0, + "AccelerationVector": "-0.005772667605 -0.993299298 0.1439166166" + }, + "XMP": { + "XMP": "base64:PD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1wOkNyZWF0ZURhdGU9IjIwMTgtMDktMTBUMTI6MTY6MTMiIHhtcDpNb2RpZnlEYXRlPSIyMDE4LTA5LTEwVDEyOjE2OjEzIiB4bXA6Q3JlYXRvclRvb2w9IlByb0NhbSAxMC41LjgiIHBob3Rvc2hvcDpEYXRlQ3JlYXRlZD0iMjAxOC0wOS0xMFQxMjoxNjoxMyIvPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDw/eHBhY2tldCBlbmQ9InciPz4A", + "XMPToolkit": "XMP Core 5.4.0", + "CreateDate": "2018:09:10 12:16:13", + "ModifyDate": "2018:09:10 12:16:13", + "CreatorTool": "ProCam 10.5.8", + "DateCreated": "2018:09:10 12:16:13" + }, + "ICC_Profile": { + "ProfileCMMType": "Apple Computer Inc.", + "ProfileVersion": "4.0.0", + "ProfileClass": "Display Device Profile", + "ColorSpaceData": "RGB ", + "ProfileConnectionSpace": "XYZ ", + "ProfileDateTime": "2017:07:07 13:22:32", + "ProfileFileSignature": "acsp", + "PrimaryPlatform": "Apple Computer Inc.", + "CMMFlags": "Not Embedded, Independent", + "DeviceManufacturer": "Apple Computer Inc.", + "DeviceModel": "", + "DeviceAttributes": "Reflective, Glossy, Positive, Color", + "RenderingIntent": "Perceptual", + "ConnectionSpaceIlluminant": "0.9642 1 0.82491", + "ProfileCreator": "Apple Computer Inc.", + "ProfileID": "ca1a9582257f104d389913d5d1ea1582", + "ProfileDescription": "Display P3", + "ProfileCopyright": "Copyright Apple Inc., 2017", + "MediaWhitePoint": "0.95045 1 1.08905", + "RedMatrixColumn": "0.51512 0.2412 -0.00105", + "GreenMatrixColumn": "0.29198 0.69225 0.04189", + "BlueMatrixColumn": "0.1571 0.06657 0.78407", + "RedTRC": "base64:cGFyYQAAAAAAAwAAAAJmZgAA8qcAAA1ZAAAT0AAACls=", + "ChromaticAdaptation": "1.04788 0.02292 -0.0502 0.02959 0.99048 -0.01706 -0.00923 0.01508 0.75168", + "BlueTRC": "base64:cGFyYQAAAAAAAwAAAAJmZgAA8qcAAA1ZAAAT0AAACls=", + "GreenTRC": "base64:cGFyYQAAAAAAAwAAAAJmZgAA8qcAAA1ZAAAT0AAACls=" + }, + "Composite": { + "Aperture": 1.8, + "GPSLatitude": "34 deg 47' 50.82\" N", + "GPSLongitude": "134 deg 45' 52.68\" E", + "GPSPosition": "34 deg 47' 50.82\" N, 134 deg 45' 52.68\" E", + "ImageSize": "4032x3024", + "Megapixels": 12.2, + "RunTimeSincePowerUp": "18:41:21", + "ScaleFactor35efl": 18.5, + "ShutterSpeed": "1/4000", + "SubSecCreateDate": "2018:09:10 12:16:13.023", + "SubSecDateTimeOriginal": "2018:09:10 12:16:13.023", + "CircleOfConfusion": "0.002 mm", + "FOV": "27.3 deg", + "FocalLength35efl": "4.0 mm (35 mm equivalent: 74.0 mm)", + "HyperfocalDistance": "5.46 m", + "LightValue": 16.0 + } +}] diff --git a/docker/webdav/files/Photos/2020/03/iphone_7.xmp b/docker/webdav/files/Photos/2020/03/iphone_7.xmp new file mode 100644 index 000000000..92dd133a3 --- /dev/null +++ b/docker/webdav/files/Photos/2020/03/iphone_7.xmp @@ -0,0 +1,101 @@ + + + + + + 13166/7763 + 11805/1036 + 65535 + + + 1 + 2 + 3 + 0 + + + 2018-09-10T12:16:13 + 1/1 + 0221 + 0/1 + 0 + 2 + 1/4000 + 9/5 + + False + False + 2 + False + 0 + + 0100 + 4/1 + 74 + 34,47.847000N + 134,45.878000E + + + 20 + + + 5 + 4032 + 3024 + 0 + 1 + 2 + 18535/1549 + + + 2009 + 1510 + 2213 + 1331 + + + 0 + + + + Apple + iPhone 7 back camera 3.99mm f/1.8 + + + 399/100 + 399/100 + 9/5 + 9/5 + + + + + + 2018-09-10T12:16:13 + + + + 3024 + 4032 + Apple + iPhone 7 + 6 + 2 + ProCam 10.5.8 + 72/1 + 72/1 + + + + 2018-09-10T12:16:13 + ProCam 10.5.8 + 2018-09-10T12:16:13 + + + + \ No newline at end of file diff --git a/docker/webdav/files/Photos/2020/03/ocean_cyan.jpg b/docker/webdav/files/Photos/2020/03/ocean_cyan.jpg new file mode 100644 index 000000000..9c345e372 Binary files /dev/null and b/docker/webdav/files/Photos/2020/03/ocean_cyan.jpg differ diff --git a/docker/webdav/files/Photos/IMG_4120.JPG b/docker/webdav/files/Photos/IMG_4120.JPG new file mode 100644 index 000000000..555e412f1 Binary files /dev/null and b/docker/webdav/files/Photos/IMG_4120.JPG differ diff --git a/docker/webdav/files/Photos/IMG_E4120.JPG b/docker/webdav/files/Photos/IMG_E4120.JPG new file mode 100644 index 000000000..2a1379a2e Binary files /dev/null and b/docker/webdav/files/Photos/IMG_E4120.JPG differ diff --git a/go.mod b/go.mod index 25bf3e6b3..885a27e21 100644 --- a/go.mod +++ b/go.mod @@ -59,6 +59,7 @@ require ( github.com/soheilhy/cmux v0.1.4 // indirect github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 // indirect github.com/stretchr/testify v1.4.0 + github.com/studio-b12/gowebdav v0.0.0-20200303150724-9380631c29a1 github.com/tensorflow/tensorflow v1.14.0 github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 // indirect github.com/twinj/uuid v1.0.0 // indirect diff --git a/go.sum b/go.sum index 9e03b34d7..4c5e06bba 100644 --- a/go.sum +++ b/go.sum @@ -308,6 +308,8 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/studio-b12/gowebdav v0.0.0-20200303150724-9380631c29a1 h1:TPyHV/OgChqNcnYqCoCvIFjR9TU60gFXXBKnhOBzVEI= +github.com/studio-b12/gowebdav v0.0.0-20200303150724-9380631c29a1/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s= github.com/tensorflow/tensorflow v1.14.0 h1:g0W2+f/RybcvmrTjPLTwXkfr/BsDGUd8FKT6ZzojOMo= github.com/tensorflow/tensorflow v1.14.0/go.mod h1:itOSERT4trABok4UOoG+X4BoKds9F3rIsySdn+Lvu90= github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 h1:lYIiVDtZnyTWlNwiAxLj0bbpTcx1BWCFhXjfsvmPdNc= diff --git a/internal/share/webdav/testdata/example.jpg b/internal/share/webdav/testdata/example.jpg new file mode 100644 index 000000000..9030b0194 Binary files /dev/null and b/internal/share/webdav/testdata/example.jpg differ diff --git a/internal/share/webdav/webdav.go b/internal/share/webdav/webdav.go new file mode 100644 index 000000000..1b76cb3bc --- /dev/null +++ b/internal/share/webdav/webdav.go @@ -0,0 +1,146 @@ +/* +Package entity implementing sharing with WebDAV servers. + +Additional information can be found in our Developer Guide: + +https://github.com/photoprism/photoprism/wiki +*/ +package webdav + +import ( + "fmt" + "io/ioutil" + "os" + "path" + + "github.com/photoprism/photoprism/internal/event" + "github.com/studio-b12/gowebdav" +) + +var log = event.Log + +type Client struct { + client *gowebdav.Client +} + +// Connect creates a new WebDAV client. +func Connect(url, user, pass string) Client { + clt := gowebdav.NewClient(url, user, pass) + + result := Client{client: clt} + + return result +} + +// Files returns all files in path as string slice. +func (c Client) Files(path string) (result []string, err error) { + files, err := c.client.ReadDir(path) + + if err != nil { + return result, err + } + + for _, file := range files { + if !file.Mode().IsRegular() { continue } + result = append(result, fmt.Sprintf("%s/%s", path, file.Name())) + } + + return result, nil +} + +// Directories returns all sub directories in path as string slice. +func (c Client) Directories(path string) (result []string, err error) { + files, err := c.client.ReadDir(path) + + if err != nil { + return result, err + } + + for _, file := range files { + if !file.Mode().IsDir() { continue } + result = append(result, fmt.Sprintf("%s/%s", path, file.Name())) + } + + return result, nil +} + +// Download downloads a single file to the given location. +func (c Client) Download(from, to string) error { + 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() { + return fmt.Errorf("webdav: %s is not a directory", dir) + } + + bytes, err := c.client.Read(from) + + if err != nil { + return err + } + + return ioutil.WriteFile(to, bytes, 0644) +} + +// DownloadDir downloads all files from a remote to a local directory. +func (c Client) DownloadDir(from, to string, recursive 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 + + if _, err := os.Stat(dest); err == nil { + // File exists + msg := fmt.Errorf("webdav: %s exists", dest) + errs = append(errs, msg) + log.Error(msg) + continue + } + + if err := c.Download(file, dest); err != nil { + msg := fmt.Errorf("webdav: %s", err) + errs = append(errs, msg) + log.Error(msg) + continue + } + } + + if !recursive { + return errs + } + + dirs, err := c.Directories(from) + + for _, dir := range dirs { + errs = append(errs, c.DownloadDir(dir, to, true)...) + } + + return errs +} + +// Upload uploads a single file to the remote server. +func (c Client) Upload(from, to string) error { + file, err := os.Open(from) + + if err != nil { + 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) +} diff --git a/internal/share/webdav/webdav_test.go b/internal/share/webdav/webdav_test.go new file mode 100644 index 000000000..ac141619b --- /dev/null +++ b/internal/share/webdav/webdav_test.go @@ -0,0 +1,115 @@ +package webdav + +import ( + "os" + "testing" + + "github.com/photoprism/photoprism/pkg/fs" + "github.com/photoprism/photoprism/pkg/rnd" + "github.com/stretchr/testify/assert" +) + +const ( + testUrl = "http://webdav-dummy/" + testUser = "admin" + testPass = "photoprism" +) + +func TestConnect(t *testing.T) { + c := Connect(testUrl, testUser, testPass) + + assert.IsType(t, Client{}, c) +} + +func TestClient_Files(t *testing.T) { + c := Connect(testUrl, testUser, testPass) + + assert.IsType(t, Client{}, c) + + files, err := c.Files("Photos") + + if err != nil { + t.Fatal(err) + } + + if len(files) == 0 { + t.Fatal("no files found") + } +} + +func TestClient_Download(t *testing.T) { + c := Connect(testUrl, testUser, testPass) + + assert.IsType(t, Client{}, c) + + files, err := c.Files("Photos") + + if err != nil { + t.Fatal(err) + } + + tempDir := os.TempDir() + rnd.UUID() + tempFile := tempDir + "/foo.jpg" + + if len(files) == 0 { + t.Fatal("no files to download") + } + + if err := c.Download(files[0], tempFile); err != nil { + t.Fatal(err) + } + + if !fs.FileExists(tempFile) { + t.Fatalf("%s does not exist", tempFile) + } + + if err := os.RemoveAll(tempDir); err != nil { + t.Fatal(err) + } +} + +func TestClient_DownloadDir(t *testing.T) { + c := Connect(testUrl, testUser, testPass) + + assert.IsType(t, Client{}, c) + + t.Run("non-recursive", func(t *testing.T) { + tempDir := os.TempDir() + rnd.UUID() + + if errs := c.DownloadDir("Photos", tempDir, false); len(errs) > 0 { + t.Fatal(errs) + } + + if err := os.RemoveAll(tempDir); err != nil { + t.Fatal(err) + } + }) + + t.Run("recursive", func(t *testing.T) { + tempDir := os.TempDir() + rnd.UUID() + + if errs := c.DownloadDir("Photos", tempDir, true); len(errs) > 0 { + t.Fatal(errs) + } + + if err := os.RemoveAll(tempDir); err != nil { + t.Fatal(err) + } + }) +} + +func TestClient_UploadAndDelete(t *testing.T) { + c := Connect(testUrl, testUser, testPass) + + assert.IsType(t, Client{}, c) + + tempName := rnd.UUID() + ".jpg" + + if err := c.Upload("testdata/example.jpg", tempName); err != nil { + t.Fatal(err) + } + + if err := c.Delete(tempName); err != nil { + t.Fatal(err) + } +}