Backend: Add WebDAV client and dummy test server #225

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-03-27 11:01:41 +01:00
parent d80a69e962
commit b86f68c3f3
16 changed files with 545 additions and 0 deletions

View file

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

View file

@ -50,3 +50,6 @@ services:
MYSQL_USER: photoprism
MYSQL_PASSWORD: photoprism
MYSQL_DATABASE: photoprism
webdav-dummy:
image: photoprism/webdav:20200327

View file

@ -53,3 +53,6 @@ services:
MYSQL_USER: photoprism
MYSQL_PASSWORD: photoprism
MYSQL_DATABASE: photoprism
webdav-dummy:
image: photoprism/webdav:20200327

16
docker/webdav/Dockerfile Normal file
View file

@ -0,0 +1,16 @@
FROM golang:1.14
LABEL maintainer="Michael Mayer <michael@liquidbytes.net>"
# 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

16
docker/webdav/config.yml Normal file
View file

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

Binary file not shown.

View file

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

View file

@ -0,0 +1,101 @@
<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 10.80'>
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
<rdf:Description rdf:about=''
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
<exif:ApertureValue>13166/7763</exif:ApertureValue>
<exif:BrightnessValue>11805/1036</exif:BrightnessValue>
<exif:ColorSpace>65535</exif:ColorSpace>
<exif:ComponentsConfiguration>
<rdf:Seq>
<rdf:li>1</rdf:li>
<rdf:li>2</rdf:li>
<rdf:li>3</rdf:li>
<rdf:li>0</rdf:li>
</rdf:Seq>
</exif:ComponentsConfiguration>
<exif:DateTimeOriginal>2018-09-10T12:16:13</exif:DateTimeOriginal>
<exif:DigitalZoomRatio>1/1</exif:DigitalZoomRatio>
<exif:ExifVersion>0221</exif:ExifVersion>
<exif:ExposureBiasValue>0/1</exif:ExposureBiasValue>
<exif:ExposureMode>0</exif:ExposureMode>
<exif:ExposureProgram>2</exif:ExposureProgram>
<exif:ExposureTime>1/4000</exif:ExposureTime>
<exif:FNumber>9/5</exif:FNumber>
<exif:Flash rdf:parseType='Resource'>
<exif:Fired>False</exif:Fired>
<exif:Function>False</exif:Function>
<exif:Mode>2</exif:Mode>
<exif:RedEyeMode>False</exif:RedEyeMode>
<exif:Return>0</exif:Return>
</exif:Flash>
<exif:FlashpixVersion>0100</exif:FlashpixVersion>
<exif:FocalLength>4/1</exif:FocalLength>
<exif:FocalLengthIn35mmFilm>74</exif:FocalLengthIn35mmFilm>
<exif:GPSLatitude>34,47.847000N</exif:GPSLatitude>
<exif:GPSLongitude>134,45.878000E</exif:GPSLongitude>
<exif:ISOSpeedRatings>
<rdf:Seq>
<rdf:li>20</rdf:li>
</rdf:Seq>
</exif:ISOSpeedRatings>
<exif:MeteringMode>5</exif:MeteringMode>
<exif:PixelXDimension>4032</exif:PixelXDimension>
<exif:PixelYDimension>3024</exif:PixelYDimension>
<exif:SceneCaptureType>0</exif:SceneCaptureType>
<exif:SceneType>1</exif:SceneType>
<exif:SensingMethod>2</exif:SensingMethod>
<exif:ShutterSpeedValue>18535/1549</exif:ShutterSpeedValue>
<exif:SubjectArea>
<rdf:Seq>
<rdf:li>2009</rdf:li>
<rdf:li>1510</rdf:li>
<rdf:li>2213</rdf:li>
<rdf:li>1331</rdf:li>
</rdf:Seq>
</exif:SubjectArea>
<exif:WhiteBalance>0</exif:WhiteBalance>
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:exifEX='http://cipa.jp/exif/1.0/'>
<exifEX:LensMake>Apple</exifEX:LensMake>
<exifEX:LensModel>iPhone 7 back camera 3.99mm f/1.8</exifEX:LensModel>
<exifEX:LensSpecification>
<rdf:Seq>
<rdf:li>399/100</rdf:li>
<rdf:li>399/100</rdf:li>
<rdf:li>9/5</rdf:li>
<rdf:li>9/5</rdf:li>
</rdf:Seq>
</exifEX:LensSpecification>
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:photoshop='http://ns.adobe.com/photoshop/1.0/'>
<photoshop:DateCreated>2018-09-10T12:16:13</photoshop:DateCreated>
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
<tiff:ImageLength>3024</tiff:ImageLength>
<tiff:ImageWidth>4032</tiff:ImageWidth>
<tiff:Make>Apple</tiff:Make>
<tiff:Model>iPhone 7</tiff:Model>
<tiff:Orientation>6</tiff:Orientation>
<tiff:ResolutionUnit>2</tiff:ResolutionUnit>
<tiff:Software>ProCam 10.5.8</tiff:Software>
<tiff:XResolution>72/1</tiff:XResolution>
<tiff:YResolution>72/1</tiff:YResolution>
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
<xmp:CreateDate>2018-09-10T12:16:13</xmp:CreateDate>
<xmp:CreatorTool>ProCam 10.5.8</xmp:CreatorTool>
<xmp:ModifyDate>2018-09-10T12:16:13</xmp:ModifyDate>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end='w'?>

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

1
go.mod
View file

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

2
go.sum
View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

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

View file

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