Merge pull request #1898 from owncloud/thumbnails_public_links

Thumbnails for public links
This commit is contained in:
David Christofas
2021-04-28 09:54:36 +02:00
committed by GitHub
37 changed files with 1166 additions and 661 deletions

View File

@@ -319,6 +319,7 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczC
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dnaeon/go-vcr v0.0.0-20180814043457-aafff18a5cc2/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/dnsimple/dnsimple-go v0.30.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
@@ -1641,6 +1642,7 @@ golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8H
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=

View File

@@ -323,6 +323,8 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczC
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dnaeon/go-vcr v0.0.0-20180814043457-aafff18a5cc2/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/dnsimple/dnsimple-go v0.30.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
@@ -1635,8 +1637,9 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=

View File

@@ -805,34 +805,11 @@ cannot share a folder with create permission
- [apiWebdavPreviews/previews.feature:17](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L17)
- [apiWebdavPreviews/previews.feature:18](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L18)
- [apiWebdavPreviews/previews.feature:19](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L19)
- [apiWebdavPreviews/previews.feature:30](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L30)
- [apiWebdavPreviews/previews.feature:31](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L31)
- [apiWebdavPreviews/previews.feature:32](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L32)
- [apiWebdavPreviews/previews.feature:33](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L33)
- [apiWebdavPreviews/previews.feature:34](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L34)
- [apiWebdavPreviews/previews.feature:35](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L35)
- [apiWebdavPreviews/previews.feature:36](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L36)
- [apiWebdavPreviews/previews.feature:47](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L47)
- [apiWebdavPreviews/previews.feature:48](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L48)
- [apiWebdavPreviews/previews.feature:49](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L49)
- [apiWebdavPreviews/previews.feature:50](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L50)
- [apiWebdavPreviews/previews.feature:51](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L51)
- [apiWebdavPreviews/previews.feature:52](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L52)
- [apiWebdavPreviews/previews.feature:53](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L53)
- [apiWebdavPreviews/previews.feature:56](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L56)
- [apiWebdavPreviews/previews.feature:71](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L71)
- [apiWebdavPreviews/previews.feature:72](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L72)
- [apiWebdavPreviews/previews.feature:73](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L73)
- [apiWebdavPreviews/previews.feature:83](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L83)
- [apiWebdavPreviews/previews.feature:84](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L84)
- [apiWebdavPreviews/previews.feature:87](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L87)
- [apiWebdavPreviews/previews.feature:95](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L95)
- [apiWebdavPreviews/previews.feature:104](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L104)
- [apiWebdavPreviews/previews.feature:113](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L113)
- [apiWebdavPreviews/previews.feature:120](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L120)
- [apiWebdavPreviews/previews.feature:127](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L127)
- [apiWebdavPreviews/previews.feature:135](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L135)
- [apiWebdavPreviews/previews.feature:144](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L144)
- [apiWebdavPreviews/previews.feature:163](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L163)
- [apiWebdavPreviews/previews.feature:164](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L164)
- [apiWebdavPreviews/previews.feature:165](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L165)

View File

@@ -829,34 +829,11 @@ cannot share a folder with create permission
- [apiWebdavPreviews/previews.feature:17](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L17)
- [apiWebdavPreviews/previews.feature:18](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L18)
- [apiWebdavPreviews/previews.feature:19](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L19)
- [apiWebdavPreviews/previews.feature:30](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L30)
- [apiWebdavPreviews/previews.feature:31](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L31)
- [apiWebdavPreviews/previews.feature:32](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L32)
- [apiWebdavPreviews/previews.feature:33](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L33)
- [apiWebdavPreviews/previews.feature:34](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L34)
- [apiWebdavPreviews/previews.feature:35](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L35)
- [apiWebdavPreviews/previews.feature:36](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L36)
- [apiWebdavPreviews/previews.feature:47](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L47)
- [apiWebdavPreviews/previews.feature:48](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L48)
- [apiWebdavPreviews/previews.feature:49](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L49)
- [apiWebdavPreviews/previews.feature:50](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L50)
- [apiWebdavPreviews/previews.feature:51](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L51)
- [apiWebdavPreviews/previews.feature:52](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L52)
- [apiWebdavPreviews/previews.feature:53](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L53)
- [apiWebdavPreviews/previews.feature:56](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L56)
- [apiWebdavPreviews/previews.feature:71](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L71)
- [apiWebdavPreviews/previews.feature:72](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L72)
- [apiWebdavPreviews/previews.feature:73](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L73)
- [apiWebdavPreviews/previews.feature:83](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L83)
- [apiWebdavPreviews/previews.feature:84](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L84)
- [apiWebdavPreviews/previews.feature:87](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L87)
- [apiWebdavPreviews/previews.feature:95](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L95)
- [apiWebdavPreviews/previews.feature:104](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L104)
- [apiWebdavPreviews/previews.feature:113](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L113)
- [apiWebdavPreviews/previews.feature:120](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L120)
- [apiWebdavPreviews/previews.feature:127](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L127)
- [apiWebdavPreviews/previews.feature:135](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L135)
- [apiWebdavPreviews/previews.feature:144](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L144)
- [apiWebdavPreviews/previews.feature:163](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L163)
- [apiWebdavPreviews/previews.feature:164](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L164)
- [apiWebdavPreviews/previews.feature:165](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavPreviews/previews.feature#L165)

View File

@@ -16,9 +16,6 @@ Other free text and markdown formatting can be used elsewhere in the document if
- [webUIPreview/imageMediaViewer.feature:140](https://github.com/owncloud/web/blob/master/tests/acceptance/features/webUIPreview/imageMediaViewer.feature#L140)
- [webUIPreview/imageMediaViewer.feature:158](https://github.com/owncloud/web/blob/master/tests/acceptance/features/webUIPreview/imageMediaViewer.feature#L158)
### [Media viewer previews are not visible in public share](https://github.com/owncloud/ocis/issues/1370)
- [webUIPreview/imageMediaViewer.feature:112](https://github.com/owncloud/web/blob/master/tests/acceptance/features/webUIPreview/imageMediaViewer.feature#L112)
### [Exit page re-appears in loop when logged in user is deleted](https://github.com/owncloud/web/issues/4677)
- [webUILogin/openidLogin.feature:53](https://github.com/owncloud/web/blob/master/tests/acceptance/features/webUILogin/openidLogin.feature#L53)
@@ -194,9 +191,6 @@ Other free text and markdown formatting can be used elsewhere in the document if
### [Can login with invalid password while logging in with openidconnect in oc10](https://github.com/owncloud/ocis/issues/1428)
- [webUILogin/openidLogin.feature:46](https://github.com/owncloud/web/blob/master/tests/acceptance/features/webUILogin/openidLogin.feature#L46)
### Image-Media-Viewer-Issue
- [webUIPreview/imageMediaViewer.feature:34](https://github.com/owncloud/web/blob/master/tests/acceptance/features/webUIPreview/imageMediaViewer.feature#L34)
### webUI-Private-Links
- [webUIPrivateLinks/accessingPrivateLinks.feature:9](https://github.com/owncloud/web/blob/master/tests/acceptance/features/webUIPrivateLinks/accessingPrivateLinks.feature#L9)
- [webUIPrivateLinks/accessingPrivateLinks.feature:17](https://github.com/owncloud/web/blob/master/tests/acceptance/features/webUIPrivateLinks/accessingPrivateLinks.feature#L17)

View File

@@ -36,37 +36,13 @@ Feature: previews of files downloaded through the webdav API
| A |
| %2F |
@issue-ocis-189
# after fixing all issues delete this Scenario and use the one from oC10 core
Scenario Outline: download previews of file types that don't support preview
Given user "Alice" has uploaded file "filesForUpload/<filename>" to "/<newfilename>"
When user "Alice" downloads the preview of "/<newfilename>" with width "32" and height "32" using the WebDAV API
Then the HTTP status code should be "400"
Examples:
| filename | newfilename |
| simple.pdf | test.pdf |
| simple.odt | test.odt |
| new-data.zip | test.zip |
@issue-ocis-187
# after fixing all issues delete this Scenario and use the one from oC10 core
Scenario Outline: download previews of different image file types
Given user "Alice" has uploaded file "filesForUpload/<imageName>" to "/<newImageName>"
When user "Alice" downloads the preview of "/<newImageName>" with width "32" and height "32" using the WebDAV API
Then the HTTP status code should be "400"
# And the downloaded image should be "1240" pixels wide and "648" pixels high
Examples:
| imageName | newImageName |
| testavatar.jpg | testimage.jpg |
| testavatar.png | testimage.png |
@issue-ocis-187
# after fixing all issues delete this Scenario and use the one from oC10 core
Scenario: download previews of image after renaming it
Given user "Alice" has uploaded file "filesForUpload/testavatar.jpg" to "/testimage.jpg"
When user "Alice" moves file "/testimage.jpg" to "/testimage.txt" using the WebDAV API
And user "Alice" downloads the preview of "/testimage.txt" with width "32" and height "32" using the WebDAV API
Then the HTTP status code should be "400"
Then the HTTP status code should be "404"
# And the downloaded image should be "1240" pixels wide and "648" pixels high
@issue-ocis-thumbnails-191 @skipOnOcis-EOS-Storage @issue-ocis-reva-308
@@ -75,7 +51,7 @@ Feature: previews of files downloaded through the webdav API
Given user "Brian" has been created with default attributes and without skeleton files
And user "Alice" has uploaded file "filesForUpload/lorem.txt" to "/parent.txt"
When user "Brian" downloads the preview of "/parent.txt" of "Alice" with width "32" and height "32" using the WebDAV API
Then the HTTP status code should be "400"
Then the HTTP status code should be "404"
@issue-ocis-190
# after fixing all issues delete this Scenario and use the one from oC10 core
@@ -90,7 +66,7 @@ Feature: previews of files downloaded through the webdav API
Given the administrator has updated system config key "enable_previews" with value "false" and type "boolean"
And user "Alice" has uploaded file "filesForUpload/lorem.txt" to "/parent.txt"
When user "Alice" downloads the preview of "/parent.txt" with width "32" and height "32" using the WebDAV API
Then the HTTP status code should be "400"
Then the HTTP status code should be "404"
@issue-ocis-193
# after fixing all issues delete this Scenario and use the one from oC10 core
@@ -99,7 +75,7 @@ Feature: previews of files downloaded through the webdav API
And the administrator has updated system config key "preview_max_x" with value "null"
And the administrator has updated system config key "preview_max_y" with value "null"
When user "Alice" downloads the preview of "/parent.txt" with width "32" and height "32" using the WebDAV API
Then the HTTP status code should be "400"
Then the HTTP status code should be "404"
@issue-ocis-193
# after fixing all issues delete this Scenario and use the one from oC10 core

View File

@@ -7,6 +7,9 @@ require (
contrib.go.opencensus.io/exporter/ocagent v0.7.0
contrib.go.opencensus.io/exporter/zipkin v0.1.2
github.com/asim/go-micro/v3 v3.5.1-0.20210217182006-0f0ace1a44a9
github.com/cs3org/go-cs3apis v0.0.0-20210325133324-32b03d75a535
github.com/cs3org/reva v1.6.1-0.20210414111318-a4b5148cbfb2
github.com/disintegration/imaging v1.6.2
github.com/golang/protobuf v1.5.2
github.com/grpc-ecosystem/grpc-gateway/v2 v2.2.0
github.com/micro/cli/v2 v2.1.2
@@ -20,7 +23,7 @@ require (
github.com/stretchr/testify v1.7.0
github.com/thejerf/suture/v4 v4.0.0
go.opencensus.io v0.23.0
golang.org/x/image v0.0.0-20190802002840-cff245a6509b
google.golang.org/grpc v1.37.0
google.golang.org/protobuf v1.26.0
)

View File

@@ -319,6 +319,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dnaeon/go-vcr v0.0.0-20180814043457-aafff18a5cc2/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/dnsimple/dnsimple-go v0.30.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
@@ -1667,8 +1669,9 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=

View File

@@ -53,12 +53,6 @@ type FileSystemStorage struct {
RootDirectory string
}
// WebDavSource defines the available webdav source configuration.
type WebDavSource struct {
BaseURL string
Insecure bool
}
// FileSystemSource defines the available filesystem source configuration.
type FileSystemSource struct {
BasePath string
@@ -66,9 +60,10 @@ type FileSystemSource struct {
// Thumbnail defines the available thumbnail related configuration.
type Thumbnail struct {
Resolutions []string
FileSystemStorage FileSystemStorage
WebDavSource WebDavSource
Resolutions []string
FileSystemStorage FileSystemStorage
WebdavAllowInsecure bool
RevaGateway string
}
// New initializes a new configuration with or without defaults.

View File

@@ -143,18 +143,18 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag {
Destination: &cfg.Thumbnail.FileSystemStorage.RootDirectory,
},
&cli.StringFlag{
Name: "webdavsource-baseurl",
Value: flags.OverrideDefaultString(cfg.Thumbnail.WebDavSource.BaseURL, "https://localhost:9200/remote.php/webdav/"),
Usage: "Base url for a webdav api",
EnvVars: []string{"THUMBNAILS_WEBDAVSOURCE_BASEURL"},
Destination: &cfg.Thumbnail.WebDavSource.BaseURL,
Name: "reva-gateway-addr",
Value: flags.OverrideDefaultString(cfg.Thumbnail.RevaGateway, "127.0.0.1:9142"),
Usage: "Reva gateway address",
EnvVars: []string{"THUMBNAILS_REVA_GATEWAY", "PROXY_REVA_GATEWAY_ADDR"},
Destination: &cfg.Thumbnail.RevaGateway,
},
&cli.BoolFlag{
Name: "webdavsource-insecure",
Value: flags.OverrideDefaultBool(cfg.Thumbnail.WebDavSource.Insecure, true),
Value: flags.OverrideDefaultBool(cfg.Thumbnail.WebdavAllowInsecure, true),
Usage: "Whether to skip certificate checks",
EnvVars: []string{"THUMBNAILS_WEBDAVSOURCE_INSECURE"},
Destination: &cfg.Thumbnail.WebDavSource.Insecure,
Destination: &cfg.Thumbnail.WebdavAllowInsecure,
},
&cli.StringSliceFlag{
Name: "thumbnail-resolution",

View File

@@ -21,55 +21,55 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// The file types to which the thumbnail cna get encoded to.
type GetRequest_FileType int32
// The file types to which the thumbnail can get encoded to.
type GetThumbnailRequest_ThumbnailType int32
const (
GetRequest_PNG GetRequest_FileType = 0 // Represents PNG type
GetRequest_JPG GetRequest_FileType = 1 // Represents JPG type
GetThumbnailRequest_PNG GetThumbnailRequest_ThumbnailType = 0 // Represents PNG type
GetThumbnailRequest_JPG GetThumbnailRequest_ThumbnailType = 1 // Represents JPG type
)
// Enum value maps for GetRequest_FileType.
// Enum value maps for GetThumbnailRequest_ThumbnailType.
var (
GetRequest_FileType_name = map[int32]string{
GetThumbnailRequest_ThumbnailType_name = map[int32]string{
0: "PNG",
1: "JPG",
}
GetRequest_FileType_value = map[string]int32{
GetThumbnailRequest_ThumbnailType_value = map[string]int32{
"PNG": 0,
"JPG": 1,
}
)
func (x GetRequest_FileType) Enum() *GetRequest_FileType {
p := new(GetRequest_FileType)
func (x GetThumbnailRequest_ThumbnailType) Enum() *GetThumbnailRequest_ThumbnailType {
p := new(GetThumbnailRequest_ThumbnailType)
*p = x
return p
}
func (x GetRequest_FileType) String() string {
func (x GetThumbnailRequest_ThumbnailType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (GetRequest_FileType) Descriptor() protoreflect.EnumDescriptor {
func (GetThumbnailRequest_ThumbnailType) Descriptor() protoreflect.EnumDescriptor {
return file_thumbnails_proto_enumTypes[0].Descriptor()
}
func (GetRequest_FileType) Type() protoreflect.EnumType {
func (GetThumbnailRequest_ThumbnailType) Type() protoreflect.EnumType {
return &file_thumbnails_proto_enumTypes[0]
}
func (x GetRequest_FileType) Number() protoreflect.EnumNumber {
func (x GetThumbnailRequest_ThumbnailType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use GetRequest_FileType.Descriptor instead.
func (GetRequest_FileType) EnumDescriptor() ([]byte, []int) {
// Deprecated: Use GetThumbnailRequest_ThumbnailType.Descriptor instead.
func (GetThumbnailRequest_ThumbnailType) EnumDescriptor() ([]byte, []int) {
return file_thumbnails_proto_rawDescGZIP(), []int{0, 0}
}
// A request to retrieve a thumbnail
type GetRequest struct {
type GetThumbnailRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
@@ -77,21 +77,19 @@ type GetRequest struct {
// The path to the source image
Filepath string `protobuf:"bytes,1,opt,name=filepath,proto3" json:"filepath,omitempty"`
// The type to which the thumbnail should get encoded to.
Filetype GetRequest_FileType `protobuf:"varint,2,opt,name=filetype,proto3,enum=com.owncloud.ocis.thumbnails.v0.GetRequest_FileType" json:"filetype,omitempty"`
// The etag of the source image
Etag string `protobuf:"bytes,3,opt,name=etag,proto3" json:"etag,omitempty"`
ThumbnailType GetThumbnailRequest_ThumbnailType `protobuf:"varint,2,opt,name=thumbnail_type,json=thumbnailType,proto3,enum=com.owncloud.ocis.thumbnails.v0.GetThumbnailRequest_ThumbnailType" json:"thumbnail_type,omitempty"`
// The width of the thumbnail
Width int32 `protobuf:"varint,4,opt,name=width,proto3" json:"width,omitempty"`
Width int32 `protobuf:"varint,3,opt,name=width,proto3" json:"width,omitempty"`
// The height of the thumbnail
Height int32 `protobuf:"varint,5,opt,name=height,proto3" json:"height,omitempty"`
// The authorization token
Authorization string `protobuf:"bytes,6,opt,name=authorization,proto3" json:"authorization,omitempty"`
// The user requesting the resource.
Username string `protobuf:"bytes,7,opt,name=username,proto3" json:"username,omitempty"`
Height int32 `protobuf:"varint,4,opt,name=height,proto3" json:"height,omitempty"`
// Types that are assignable to Source:
// *GetThumbnailRequest_WebdavSource
// *GetThumbnailRequest_Cs3Source
Source isGetThumbnailRequest_Source `protobuf_oneof:"source"`
}
func (x *GetRequest) Reset() {
*x = GetRequest{}
func (x *GetThumbnailRequest) Reset() {
*x = GetThumbnailRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_thumbnails_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -99,13 +97,13 @@ func (x *GetRequest) Reset() {
}
}
func (x *GetRequest) String() string {
func (x *GetThumbnailRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetRequest) ProtoMessage() {}
func (*GetThumbnailRequest) ProtoMessage() {}
func (x *GetRequest) ProtoReflect() protoreflect.Message {
func (x *GetThumbnailRequest) ProtoReflect() protoreflect.Message {
mi := &file_thumbnails_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -117,74 +115,95 @@ func (x *GetRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use GetRequest.ProtoReflect.Descriptor instead.
func (*GetRequest) Descriptor() ([]byte, []int) {
// Deprecated: Use GetThumbnailRequest.ProtoReflect.Descriptor instead.
func (*GetThumbnailRequest) Descriptor() ([]byte, []int) {
return file_thumbnails_proto_rawDescGZIP(), []int{0}
}
func (x *GetRequest) GetFilepath() string {
func (x *GetThumbnailRequest) GetFilepath() string {
if x != nil {
return x.Filepath
}
return ""
}
func (x *GetRequest) GetFiletype() GetRequest_FileType {
func (x *GetThumbnailRequest) GetThumbnailType() GetThumbnailRequest_ThumbnailType {
if x != nil {
return x.Filetype
return x.ThumbnailType
}
return GetRequest_PNG
return GetThumbnailRequest_PNG
}
func (x *GetRequest) GetEtag() string {
if x != nil {
return x.Etag
}
return ""
}
func (x *GetRequest) GetWidth() int32 {
func (x *GetThumbnailRequest) GetWidth() int32 {
if x != nil {
return x.Width
}
return 0
}
func (x *GetRequest) GetHeight() int32 {
func (x *GetThumbnailRequest) GetHeight() int32 {
if x != nil {
return x.Height
}
return 0
}
func (x *GetRequest) GetAuthorization() string {
if x != nil {
return x.Authorization
func (m *GetThumbnailRequest) GetSource() isGetThumbnailRequest_Source {
if m != nil {
return m.Source
}
return ""
return nil
}
func (x *GetRequest) GetUsername() string {
if x != nil {
return x.Username
func (x *GetThumbnailRequest) GetWebdavSource() *WebdavSource {
if x, ok := x.GetSource().(*GetThumbnailRequest_WebdavSource); ok {
return x.WebdavSource
}
return ""
return nil
}
// The service response
type GetResponse struct {
func (x *GetThumbnailRequest) GetCs3Source() *CS3Source {
if x, ok := x.GetSource().(*GetThumbnailRequest_Cs3Source); ok {
return x.Cs3Source
}
return nil
}
type isGetThumbnailRequest_Source interface {
isGetThumbnailRequest_Source()
}
type GetThumbnailRequest_WebdavSource struct {
WebdavSource *WebdavSource `protobuf:"bytes,5,opt,name=webdav_source,json=webdavSource,proto3,oneof"`
}
type GetThumbnailRequest_Cs3Source struct {
Cs3Source *CS3Source `protobuf:"bytes,6,opt,name=cs3_source,json=cs3Source,proto3,oneof"`
}
func (*GetThumbnailRequest_WebdavSource) isGetThumbnailRequest_Source() {}
func (*GetThumbnailRequest_Cs3Source) isGetThumbnailRequest_Source() {}
type WebdavSource struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// The thumbnail as a binary
Thumbnail []byte `protobuf:"bytes,1,opt,name=thumbnail,proto3" json:"thumbnail,omitempty"`
// The mimetype of the thumbnail
Mimetype string `protobuf:"bytes,2,opt,name=mimetype,proto3" json:"mimetype,omitempty"`
// REQUIRED.
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
// REQUIRED.
IsPublicLink bool `protobuf:"varint,2,opt,name=is_public_link,json=isPublicLink,proto3" json:"is_public_link,omitempty"`
// OPTIONAL.
WebdavAuthorization string `protobuf:"bytes,3,opt,name=webdav_authorization,json=webdavAuthorization,proto3" json:"webdav_authorization,omitempty"`
// OPTIONAL.
RevaAuthorization string `protobuf:"bytes,4,opt,name=reva_authorization,json=revaAuthorization,proto3" json:"reva_authorization,omitempty"`
// OPTIONAL.
PublicLinkToken string `protobuf:"bytes,5,opt,name=public_link_token,json=publicLinkToken,proto3" json:"public_link_token,omitempty"`
}
func (x *GetResponse) Reset() {
*x = GetResponse{}
func (x *WebdavSource) Reset() {
*x = WebdavSource{}
if protoimpl.UnsafeEnabled {
mi := &file_thumbnails_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -192,13 +211,13 @@ func (x *GetResponse) Reset() {
}
}
func (x *GetResponse) String() string {
func (x *WebdavSource) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetResponse) ProtoMessage() {}
func (*WebdavSource) ProtoMessage() {}
func (x *GetResponse) ProtoReflect() protoreflect.Message {
func (x *WebdavSource) ProtoReflect() protoreflect.Message {
mi := &file_thumbnails_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -210,19 +229,153 @@ func (x *GetResponse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use GetResponse.ProtoReflect.Descriptor instead.
func (*GetResponse) Descriptor() ([]byte, []int) {
// Deprecated: Use WebdavSource.ProtoReflect.Descriptor instead.
func (*WebdavSource) Descriptor() ([]byte, []int) {
return file_thumbnails_proto_rawDescGZIP(), []int{1}
}
func (x *GetResponse) GetThumbnail() []byte {
func (x *WebdavSource) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
func (x *WebdavSource) GetIsPublicLink() bool {
if x != nil {
return x.IsPublicLink
}
return false
}
func (x *WebdavSource) GetWebdavAuthorization() string {
if x != nil {
return x.WebdavAuthorization
}
return ""
}
func (x *WebdavSource) GetRevaAuthorization() string {
if x != nil {
return x.RevaAuthorization
}
return ""
}
func (x *WebdavSource) GetPublicLinkToken() string {
if x != nil {
return x.PublicLinkToken
}
return ""
}
type CS3Source struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
Authorization string `protobuf:"bytes,2,opt,name=authorization,proto3" json:"authorization,omitempty"`
}
func (x *CS3Source) Reset() {
*x = CS3Source{}
if protoimpl.UnsafeEnabled {
mi := &file_thumbnails_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CS3Source) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CS3Source) ProtoMessage() {}
func (x *CS3Source) ProtoReflect() protoreflect.Message {
mi := &file_thumbnails_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CS3Source.ProtoReflect.Descriptor instead.
func (*CS3Source) Descriptor() ([]byte, []int) {
return file_thumbnails_proto_rawDescGZIP(), []int{2}
}
func (x *CS3Source) GetPath() string {
if x != nil {
return x.Path
}
return ""
}
func (x *CS3Source) GetAuthorization() string {
if x != nil {
return x.Authorization
}
return ""
}
// The service response
type GetThumbnailResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// The thumbnail as a binary
Thumbnail []byte `protobuf:"bytes,1,opt,name=thumbnail,proto3" json:"thumbnail,omitempty"`
// The mimetype of the thumbnail
Mimetype string `protobuf:"bytes,2,opt,name=mimetype,proto3" json:"mimetype,omitempty"`
}
func (x *GetThumbnailResponse) Reset() {
*x = GetThumbnailResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_thumbnails_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetThumbnailResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetThumbnailResponse) ProtoMessage() {}
func (x *GetThumbnailResponse) ProtoReflect() protoreflect.Message {
mi := &file_thumbnails_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetThumbnailResponse.ProtoReflect.Descriptor instead.
func (*GetThumbnailResponse) Descriptor() ([]byte, []int) {
return file_thumbnails_proto_rawDescGZIP(), []int{3}
}
func (x *GetThumbnailResponse) GetThumbnail() []byte {
if x != nil {
return x.Thumbnail
}
return nil
}
func (x *GetResponse) GetMimetype() string {
func (x *GetThumbnailResponse) GetMimetype() string {
if x != nil {
return x.Mimetype
}
@@ -238,60 +391,87 @@ var file_thumbnails_proto_rawDesc = []byte{
0x2e, 0x76, 0x30, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d,
0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x76, 0x32, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e,
0x73, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x22, 0x9c, 0x02, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x70, 0x61, 0x74, 0x68, 0x12, 0x50,
0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e,
0x32, 0x34, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e,
0x6f, 0x63, 0x69, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2e,
0x76, 0x30, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x46, 0x69,
0x6c, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x74, 0x79, 0x70, 0x65,
0x12, 0x12, 0x0a, 0x04, 0x65, 0x74, 0x61, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x65, 0x74, 0x61, 0x67, 0x12, 0x14, 0x0a, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x18, 0x04, 0x20,
0x01, 0x28, 0x05, 0x52, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65,
0x69, 0x67, 0x68, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67,
0x68, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x6f,
0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72,
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72,
0x6e, 0x61, 0x6d, 0x65, 0x22, 0x1c, 0x0a, 0x08, 0x46, 0x69, 0x6c, 0x65, 0x54, 0x79, 0x70, 0x65,
0x12, 0x07, 0x0a, 0x03, 0x50, 0x4e, 0x47, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4a, 0x50, 0x47,
0x10, 0x01, 0x22, 0x47, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x18, 0x01,
0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x12,
0x1a, 0x0a, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x74, 0x79, 0x70, 0x65, 0x32, 0x7d, 0x0a, 0x10, 0x54,
0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
0x69, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x12,
0x2b, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6f,
0x6f, 0x74, 0x6f, 0x22, 0x9a, 0x03, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x54, 0x68, 0x75, 0x6d, 0x62,
0x6e, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x66,
0x69, 0x6c, 0x65, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66,
0x69, 0x6c, 0x65, 0x70, 0x61, 0x74, 0x68, 0x12, 0x69, 0x0a, 0x0e, 0x74, 0x68, 0x75, 0x6d, 0x62,
0x6e, 0x61, 0x69, 0x6c, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32,
0x42, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6f,
0x63, 0x69, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2e, 0x76,
0x30, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63,
0x6f, 0x6d, 0x2e, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6f, 0x63, 0x69, 0x73,
0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2e, 0x76, 0x30, 0x2e, 0x47,
0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0xe0, 0x02, 0x5a, 0x36, 0x67,
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f,
0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69,
0x6c, 0x73, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x30, 0x3b,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x92, 0x41, 0xa4, 0x02, 0x12, 0xb8, 0x01, 0x0a, 0x22, 0x6f, 0x77,
0x6e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x20, 0x49, 0x6e, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x65, 0x20,
0x53, 0x63, 0x61, 0x6c, 0x65, 0x20, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73,
0x22, 0x47, 0x0a, 0x0d, 0x6f, 0x77, 0x6e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x20, 0x47, 0x6d, 0x62,
0x48, 0x12, 0x20, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f,
0x63, 0x69, 0x73, 0x1a, 0x14, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x40, 0x6f, 0x77, 0x6e,
0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x63, 0x6f, 0x6d, 0x2a, 0x42, 0x0a, 0x0a, 0x41, 0x70, 0x61,
0x63, 0x68, 0x65, 0x2d, 0x32, 0x2e, 0x30, 0x12, 0x34, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f,
0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e, 0x63,
0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x62, 0x6c, 0x6f, 0x62, 0x2f, 0x6d,
0x61, 0x73, 0x74, 0x65, 0x72, 0x2f, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x32, 0x05, 0x31,
0x2e, 0x30, 0x2e, 0x30, 0x2a, 0x02, 0x01, 0x02, 0x32, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x10, 0x61, 0x70, 0x70, 0x6c,
0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x72, 0x3f, 0x0a, 0x10,
0x44, 0x65, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x72, 0x20, 0x4d, 0x61, 0x6e, 0x75, 0x61, 0x6c,
0x12, 0x2b, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f,
0x75, 0x64, 0x2e, 0x64, 0x65, 0x76, 0x2f, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e,
0x73, 0x2f, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2f, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
0x30, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x54,
0x79, 0x70, 0x65, 0x52, 0x0d, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x54, 0x79,
0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28,
0x05, 0x52, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67,
0x68, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74,
0x12, 0x54, 0x0a, 0x0d, 0x77, 0x65, 0x62, 0x64, 0x61, 0x76, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x6f, 0x77,
0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d,
0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2e, 0x76, 0x30, 0x2e, 0x57, 0x65, 0x62, 0x64, 0x61, 0x76,
0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x00, 0x52, 0x0c, 0x77, 0x65, 0x62, 0x64, 0x61, 0x76,
0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x4b, 0x0a, 0x0a, 0x63, 0x73, 0x33, 0x5f, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6d,
0x2e, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x74,
0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2e, 0x76, 0x30, 0x2e, 0x43, 0x53, 0x33,
0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x00, 0x52, 0x09, 0x63, 0x73, 0x33, 0x53, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x22, 0x21, 0x0a, 0x0d, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c,
0x54, 0x79, 0x70, 0x65, 0x12, 0x07, 0x0a, 0x03, 0x50, 0x4e, 0x47, 0x10, 0x00, 0x12, 0x07, 0x0a,
0x03, 0x4a, 0x50, 0x47, 0x10, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x22, 0xd4, 0x01, 0x0a, 0x0c, 0x57, 0x65, 0x62, 0x64, 0x61, 0x76, 0x53, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
0x75, 0x72, 0x6c, 0x12, 0x24, 0x0a, 0x0e, 0x69, 0x73, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63,
0x5f, 0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x69, 0x73, 0x50,
0x75, 0x62, 0x6c, 0x69, 0x63, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x31, 0x0a, 0x14, 0x77, 0x65, 0x62,
0x64, 0x61, 0x76, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, 0x65, 0x62, 0x64, 0x61, 0x76, 0x41,
0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x0a, 0x12,
0x72, 0x65, 0x76, 0x61, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x72, 0x65, 0x76, 0x61, 0x41, 0x75,
0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2a, 0x0a, 0x11, 0x70,
0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6c, 0x69, 0x6e, 0x6b, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e,
0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4c, 0x69,
0x6e, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x45, 0x0a, 0x09, 0x43, 0x53, 0x33, 0x53, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x24, 0x0a, 0x0d, 0x61, 0x75, 0x74, 0x68,
0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x50,
0x0a, 0x14, 0x47, 0x65, 0x74, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e,
0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x74, 0x68, 0x75, 0x6d, 0x62,
0x6e, 0x61, 0x69, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x74, 0x79, 0x70, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x74, 0x79, 0x70, 0x65,
0x32, 0x8f, 0x01, 0x0a, 0x10, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x53, 0x65,
0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7b, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x54, 0x68, 0x75, 0x6d,
0x62, 0x6e, 0x61, 0x69, 0x6c, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x6f, 0x77, 0x6e, 0x63,
0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e,
0x61, 0x69, 0x6c, 0x73, 0x2e, 0x76, 0x30, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x68, 0x75, 0x6d, 0x62,
0x6e, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f,
0x6d, 0x2e, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e,
0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2e, 0x76, 0x30, 0x2e, 0x47, 0x65,
0x74, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x42, 0xe0, 0x02, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
0x6d, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f,
0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x30, 0x3b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x92, 0x41, 0xa4,
0x02, 0x12, 0xb8, 0x01, 0x0a, 0x22, 0x6f, 0x77, 0x6e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x20, 0x49,
0x6e, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x65, 0x20, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x20, 0x74, 0x68,
0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x47, 0x0a, 0x0d, 0x6f, 0x77, 0x6e, 0x43,
0x6c, 0x6f, 0x75, 0x64, 0x20, 0x47, 0x6d, 0x62, 0x48, 0x12, 0x20, 0x68, 0x74, 0x74, 0x70, 0x73,
0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77,
0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x1a, 0x14, 0x73, 0x75, 0x70,
0x70, 0x6f, 0x72, 0x74, 0x40, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x63, 0x6f,
0x6d, 0x2a, 0x42, 0x0a, 0x0a, 0x41, 0x70, 0x61, 0x63, 0x68, 0x65, 0x2d, 0x32, 0x2e, 0x30, 0x12,
0x34, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69,
0x73, 0x2f, 0x62, 0x6c, 0x6f, 0x62, 0x2f, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x2f, 0x4c, 0x49,
0x43, 0x45, 0x4e, 0x53, 0x45, 0x32, 0x05, 0x31, 0x2e, 0x30, 0x2e, 0x30, 0x2a, 0x02, 0x01, 0x02,
0x32, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73,
0x6f, 0x6e, 0x3a, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f,
0x6a, 0x73, 0x6f, 0x6e, 0x72, 0x3f, 0x0a, 0x10, 0x44, 0x65, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65,
0x72, 0x20, 0x4d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x12, 0x2b, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a,
0x2f, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x64, 0x65, 0x76, 0x2f, 0x65,
0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e,
0x61, 0x69, 0x6c, 0x73, 0x2f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -307,21 +487,25 @@ func file_thumbnails_proto_rawDescGZIP() []byte {
}
var file_thumbnails_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_thumbnails_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_thumbnails_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_thumbnails_proto_goTypes = []interface{}{
(GetRequest_FileType)(0), // 0: com.owncloud.ocis.thumbnails.v0.GetRequest.FileType
(*GetRequest)(nil), // 1: com.owncloud.ocis.thumbnails.v0.GetRequest
(*GetResponse)(nil), // 2: com.owncloud.ocis.thumbnails.v0.GetResponse
(GetThumbnailRequest_ThumbnailType)(0), // 0: com.owncloud.ocis.thumbnails.v0.GetThumbnailRequest.ThumbnailType
(*GetThumbnailRequest)(nil), // 1: com.owncloud.ocis.thumbnails.v0.GetThumbnailRequest
(*WebdavSource)(nil), // 2: com.owncloud.ocis.thumbnails.v0.WebdavSource
(*CS3Source)(nil), // 3: com.owncloud.ocis.thumbnails.v0.CS3Source
(*GetThumbnailResponse)(nil), // 4: com.owncloud.ocis.thumbnails.v0.GetThumbnailResponse
}
var file_thumbnails_proto_depIdxs = []int32{
0, // 0: com.owncloud.ocis.thumbnails.v0.GetRequest.filetype:type_name -> com.owncloud.ocis.thumbnails.v0.GetRequest.FileType
1, // 1: com.owncloud.ocis.thumbnails.v0.ThumbnailService.GetThumbnail:input_type -> com.owncloud.ocis.thumbnails.v0.GetRequest
2, // 2: com.owncloud.ocis.thumbnails.v0.ThumbnailService.GetThumbnail:output_type -> com.owncloud.ocis.thumbnails.v0.GetResponse
2, // [2:3] is the sub-list for method output_type
1, // [1:2] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
0, // 0: com.owncloud.ocis.thumbnails.v0.GetThumbnailRequest.thumbnail_type:type_name -> com.owncloud.ocis.thumbnails.v0.GetThumbnailRequest.ThumbnailType
2, // 1: com.owncloud.ocis.thumbnails.v0.GetThumbnailRequest.webdav_source:type_name -> com.owncloud.ocis.thumbnails.v0.WebdavSource
3, // 2: com.owncloud.ocis.thumbnails.v0.GetThumbnailRequest.cs3_source:type_name -> com.owncloud.ocis.thumbnails.v0.CS3Source
1, // 3: com.owncloud.ocis.thumbnails.v0.ThumbnailService.GetThumbnail:input_type -> com.owncloud.ocis.thumbnails.v0.GetThumbnailRequest
4, // 4: com.owncloud.ocis.thumbnails.v0.ThumbnailService.GetThumbnail:output_type -> com.owncloud.ocis.thumbnails.v0.GetThumbnailResponse
4, // [4:5] is the sub-list for method output_type
3, // [3:4] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_thumbnails_proto_init() }
@@ -331,7 +515,7 @@ func file_thumbnails_proto_init() {
}
if !protoimpl.UnsafeEnabled {
file_thumbnails_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetRequest); i {
switch v := v.(*GetThumbnailRequest); i {
case 0:
return &v.state
case 1:
@@ -343,7 +527,31 @@ func file_thumbnails_proto_init() {
}
}
file_thumbnails_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetResponse); i {
switch v := v.(*WebdavSource); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_thumbnails_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CS3Source); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_thumbnails_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetThumbnailResponse); i {
case 0:
return &v.state
case 1:
@@ -355,13 +563,17 @@ func file_thumbnails_proto_init() {
}
}
}
file_thumbnails_proto_msgTypes[0].OneofWrappers = []interface{}{
(*GetThumbnailRequest_WebdavSource)(nil),
(*GetThumbnailRequest_Cs3Source)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_thumbnails_proto_rawDesc,
NumEnums: 1,
NumMessages: 2,
NumMessages: 4,
NumExtensions: 0,
NumServices: 1,
},

View File

@@ -44,7 +44,7 @@ func NewThumbnailServiceEndpoints() []*api.Endpoint {
type ThumbnailService interface {
// Generates the thumbnail and returns it.
GetThumbnail(ctx context.Context, in *GetRequest, opts ...client.CallOption) (*GetResponse, error)
GetThumbnail(ctx context.Context, in *GetThumbnailRequest, opts ...client.CallOption) (*GetThumbnailResponse, error)
}
type thumbnailService struct {
@@ -59,9 +59,9 @@ func NewThumbnailService(name string, c client.Client) ThumbnailService {
}
}
func (c *thumbnailService) GetThumbnail(ctx context.Context, in *GetRequest, opts ...client.CallOption) (*GetResponse, error) {
func (c *thumbnailService) GetThumbnail(ctx context.Context, in *GetThumbnailRequest, opts ...client.CallOption) (*GetThumbnailResponse, error) {
req := c.c.NewRequest(c.name, "ThumbnailService.GetThumbnail", in)
out := new(GetResponse)
out := new(GetThumbnailResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
@@ -73,12 +73,12 @@ func (c *thumbnailService) GetThumbnail(ctx context.Context, in *GetRequest, opt
type ThumbnailServiceHandler interface {
// Generates the thumbnail and returns it.
GetThumbnail(context.Context, *GetRequest, *GetResponse) error
GetThumbnail(context.Context, *GetThumbnailRequest, *GetThumbnailResponse) error
}
func RegisterThumbnailServiceHandler(s server.Server, hdlr ThumbnailServiceHandler, opts ...server.HandlerOption) error {
type thumbnailService interface {
GetThumbnail(ctx context.Context, in *GetRequest, out *GetResponse) error
GetThumbnail(ctx context.Context, in *GetThumbnailRequest, out *GetThumbnailResponse) error
}
type ThumbnailService struct {
thumbnailService
@@ -91,6 +91,6 @@ type thumbnailServiceHandler struct {
ThumbnailServiceHandler
}
func (h *thumbnailServiceHandler) GetThumbnail(ctx context.Context, in *GetRequest, out *GetResponse) error {
func (h *thumbnailServiceHandler) GetThumbnail(ctx context.Context, in *GetThumbnailRequest, out *GetThumbnailResponse) error {
return h.ThumbnailServiceHandler.GetThumbnail(ctx, in, out)
}

View File

@@ -1,9 +1,7 @@
package proto_test
import (
"bytes"
"context"
"image"
"log"
"os"
"path/filepath"
@@ -52,13 +50,11 @@ func init() {
}
func TestGetThumbnailInvalidImage(t *testing.T) {
req := proto.GetRequest{
req := proto.GetThumbnailRequest{
Filepath: "invalid.png",
Filetype: proto.GetRequest_PNG,
Etag: "33a64df551425fcc55e4d42a148795d9f25f89d4",
ThumbnailType: proto.GetThumbnailRequest_PNG,
Height: 32,
Width: 32,
Username: "user1",
}
client := service.Client()
cl := proto.NewThumbnailService("com.owncloud.api.thumbnails", client)
@@ -67,26 +63,24 @@ func TestGetThumbnailInvalidImage(t *testing.T) {
assert.NotNil(t, err)
}
func TestGetThumbnail(t *testing.T) {
req := proto.GetRequest{
Filepath: "oc.png",
Filetype: proto.GetRequest_PNG,
Etag: "33a64df551425fcc55e4d42a148795d9f25f89d4",
Height: 32,
Width: 32,
Authorization: "Bearer eyJhbGciOiJQUzI1NiIsImtpZCI6IiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJwaG9lbml4IiwiZXhwIjoxNTkwNTc1Mzk4LCJqdGkiOiJqUEw5c1A3UUEzY0diYi1yRnhkSjJCWnFPc1BDTDg1ZyIsImlhdCI6MTU5MDU3NDc5OCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6OTIwMCIsInN1YiI6Ilh0U2lfbWl5V1NCLXBrdkdueFBvQzVBNGZsaWgwVUNMZ3ZVN2NMd2ptakNLWDdGWW4ySFdrNnJSQ0V1eTJHNXFBeV95TVFjX0ZLOWFORmhVTXJYMnBRQGtvbm5lY3QiLCJrYy5pc0FjY2Vzc1Rva2VuIjp0cnVlLCJrYy5hdXRob3JpemVkU2NvcGVzIjpbIm9wZW5pZCIsInByb2ZpbGUiLCJlbWFpbCJdLCJrYy5pZGVudGl0eSI6eyJrYy5pLmRuIjoiRWluc3RlaW4iLCJrYy5pLmlkIjoiY249ZWluc3RlaW4sb3U9dXNlcnMsZGM9ZXhhbXBsZSxkYz1vcmciLCJrYy5pLnVuIjoiZWluc3RlaW4ifSwia2MucHJvdmlkZXIiOiJpZGVudGlmaWVyLWxkYXAifQ.FSDe4vzwYpHbNfckBON5EI-01MS_dYFxenddqfJPzjlAEMEH2FFn2xQHCsxhC7wSxivhjV7Z5eRoNUR606keA64Tjs8pJBNECSptBMmE_xfAlc6X5IFILgDnR5bBu6Z2hhu-dVj72Hcyvo_X__OeWekYu7oyoXW41Mw3ayiUAwjCAzV3WPOAJ_r0zbW68_m29BgH3BoSxaF6lmjStIIAIyw7IBZ2QXb_FvGouknmfeWlGL9lkFPGL_dYKwjWieG947nY4Kg8IvHByEbw-xlY3L2EdA7Q8ZMbqdX7GzjtEIVYvCT4-TxWRcmB3SmO-Z8CVq27NHlKm3aZ0k2PS8Ga1w",
Username: "user1",
}
client := service.Client()
cl := proto.NewThumbnailService("com.owncloud.api.thumbnails", client)
rsp, err := cl.GetThumbnail(context.Background(), &req)
if err != nil {
log.Fatalf("error %s", err.Error())
}
assert.NotEmpty(t, rsp.GetThumbnail())
img, _, _ := image.Decode(bytes.NewReader(rsp.GetThumbnail()))
assert.Equal(t, 32, img.Bounds().Size().X)
assert.Equal(t, "image/png", rsp.GetMimetype())
}
// TODO(corby) update tests
//func TestGetThumbnail(t *testing.T) {
// req := proto.GetThumbnailRequest{
// Filepath: "oc.png",
// ThumbnailType: proto.GetThumbnailRequest_PNG,
// Height: 32,
// Width: 32,
// }
// client := service.Client()
// cl := proto.NewThumbnailService("com.owncloud.api.thumbnails", client)
// rsp, err := cl.GetThumbnail(context.Background(), &req)
// if err != nil {
// log.Fatalf("error %s", err.Error())
// }
// assert.NotEmpty(t, rsp.GetThumbnail())
//
// img, _, _ := image.Decode(bytes.NewReader(rsp.GetThumbnail()))
// assert.Equal(t, 32, img.Bounds().Size().X)
//
// assert.Equal(t, "image/png", rsp.GetMimetype())
//}

View File

@@ -10,19 +10,18 @@ import (
type TestRequest struct {
testDataName string
filepath string
filetype proto.GetRequest_FileType
etag string
filetype proto.GetThumbnailRequest_ThumbnailType
Checksum string
width int32
height int32
authorization string
expected proto.GetRequest
expected proto.GetThumbnailRequest
}
type TestResponse struct {
testDataName string
img []byte
mimetype string
expected proto.GetResponse
expected proto.GetThumbnailResponse
}
func TestRequestString(t *testing.T) {
@@ -31,64 +30,53 @@ func TestRequestString(t *testing.T) {
{
"ASCII",
"Foo.jpg",
proto.GetRequest_JPG,
proto.GetThumbnailRequest_JPG,
"33a64df551425fcc55e4d42a148795d9f25f89d4",
24,
24,
"Basic SGVXaG9SZWFkc1RoaXM6SXNTdHVwaWQK",
proto.GetRequest{
proto.GetThumbnailRequest{
Filepath: "Foo.jpg",
Filetype: proto.GetRequest_JPG,
Etag: "33a64df551425fcc55e4d42a148795d9f25f89d4",
ThumbnailType: proto.GetThumbnailRequest_JPG,
Width: 24,
Height: 24,
Authorization: "Basic SGVXaG9SZWFkc1RoaXM6SXNTdHVwaWQK",
},
},
{
"UTF",
"मिलन.jpg",
proto.GetRequest_JPG,
proto.GetThumbnailRequest_JPG,
"33a64df551425fcc55e4d42a148795d9f25f89d4",
24,
24,
"Basic SGVXaG9SZWFkc1RoaXM6SXNTdHVwaWQK",
proto.GetRequest{
proto.GetThumbnailRequest{
Filepath: "\340\244\256\340\244\277\340\244\262\340\244\250.jpg",
Filetype: proto.GetRequest_JPG,
Etag: "33a64df551425fcc55e4d42a148795d9f25f89d4",
ThumbnailType: proto.GetThumbnailRequest_JPG,
Width: 24,
Height: 24,
Authorization: "Basic SGVXaG9SZWFkc1RoaXM6SXNTdHVwaWQK",
},
},
{
"PNG",
"Foo.png",
proto.GetRequest_PNG,
proto.GetThumbnailRequest_PNG,
"33a64df551425fcc55e4d42a148795d9f25f89d4",
24,
24,
"Basic SGVXaG9SZWFkc1RoaXM6SXNTdHVwaWQK",
proto.GetRequest{
Filepath: "Foo.png",
Etag: "33a64df551425fcc55e4d42a148795d9f25f89d4",
Width: 24,
Height: 24,
Authorization: "Basic SGVXaG9SZWFkc1RoaXM6SXNTdHVwaWQK",
proto.GetThumbnailRequest{
Filepath: "Foo.png",
Width: 24,
Height: 24,
},
},
}
for _, testCase := range tests {
t.Run(testCase.testDataName, func(t *testing.T) {
req := proto.GetRequest{
req := proto.GetThumbnailRequest{
Filepath: testCase.filepath,
Filetype: testCase.filetype,
Etag: testCase.etag,
ThumbnailType: testCase.filetype,
Height: testCase.height,
Width: testCase.width,
Authorization: testCase.authorization,
}
assert.Equal(t, testCase.expected.String(), req.String())
})
@@ -101,7 +89,7 @@ func TestResponseString(t *testing.T) {
"ASCII",
[]byte("image data"),
"image/png",
proto.GetResponse{
proto.GetThumbnailResponse{
Thumbnail: []byte("image data"),
Mimetype: "image/png",
},
@@ -110,7 +98,7 @@ func TestResponseString(t *testing.T) {
for _, testCase := range tests {
t.Run(testCase.testDataName, func(t *testing.T) {
response := proto.GetResponse{
response := proto.GetThumbnailResponse{
Thumbnail: testCase.img,
Mimetype: testCase.mimetype,
}

View File

@@ -33,36 +33,52 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
// A Service for handling thumbnail generation
service ThumbnailService {
// Generates the thumbnail and returns it.
rpc GetThumbnail(GetRequest) returns (GetResponse);
rpc GetThumbnail(GetThumbnailRequest) returns (GetThumbnailResponse);
}
// A request to retrieve a thumbnail
message GetRequest {
message GetThumbnailRequest {
// The path to the source image
string filepath = 1;
// The file types to which the thumbnail cna get encoded to.
enum FileType {
// The file types to which the thumbnail can get encoded to.
enum ThumbnailType {
PNG = 0; // Represents PNG type
JPG = 1; // Represents JPG type
}
// The type to which the thumbnail should get encoded to.
FileType filetype = 2;
// The etag of the source image
string etag = 3;
ThumbnailType thumbnail_type = 2;
// The width of the thumbnail
int32 width = 4;
int32 width = 3;
// The height of the thumbnail
int32 height = 5;
// The authorization token
string authorization = 6;
// The user requesting the resource.
string username = 7;
int32 height = 4;
oneof source {
WebdavSource webdav_source = 5;
CS3Source cs3_source = 6;
}
}
message WebdavSource {
// REQUIRED.
string url = 1;
// REQUIRED.
bool is_public_link = 2;
// OPTIONAL.
string webdav_authorization = 3;
// OPTIONAL.
string reva_authorization = 4;
// OPTIONAL.
string public_link_token = 5;
}
message CS3Source {
string path = 1;
string authorization = 2;
}
// The service response
message GetResponse {
message GetThumbnailResponse {
// The thumbnail as a binary
bytes thumbnail = 1;
// The mimetype of the thumbnail
string mimetype = 2;
}
}

View File

@@ -1,6 +1,7 @@
package grpc
import (
"github.com/cs3org/reva/pkg/rgrpc/todo/pool"
"github.com/owncloud/ocis/ocis-pkg/service/grpc"
"github.com/owncloud/ocis/thumbnails/pkg/proto/v0"
svc "github.com/owncloud/ocis/thumbnails/pkg/service/v0"
@@ -23,19 +24,26 @@ func NewService(opts ...Option) grpc.Service {
grpc.Flags(options.Flags...),
grpc.Version(options.Config.Server.Version),
)
tconf := options.Config.Thumbnail
gc, err := pool.GetGatewayServiceClient(tconf.RevaGateway)
if err != nil {
options.Logger.Error().Err(err).Msg("could not get gateway client")
return grpc.Service{}
}
var thumbnail proto.ThumbnailServiceHandler
{
thumbnail = svc.NewService(
svc.Config(options.Config),
svc.Logger(options.Logger),
svc.ThumbnailSource(imgsource.NewWebDavSource(options.Config.Thumbnail.WebDavSource)),
svc.ThumbnailSource(imgsource.NewWebDavSource(tconf)),
svc.ThumbnailStorage(
storage.NewFileSystemStorage(
options.Config.Thumbnail.FileSystemStorage,
tconf.FileSystemStorage,
options.Logger,
),
),
svc.CS3Source(imgsource.NewCS3Source(gc)),
svc.CS3Client(gc),
)
thumbnail = svc.NewInstrument(thumbnail, options.Metrics)
thumbnail = svc.NewLogging(thumbnail, options.Logger)

View File

@@ -22,7 +22,7 @@ type instrument struct {
}
// GetThumbnail implements the ThumbnailServiceHandler interface.
func (i instrument) GetThumbnail(ctx context.Context, req *v0proto.GetRequest, rsp *v0proto.GetResponse) error {
func (i instrument) GetThumbnail(ctx context.Context, req *v0proto.GetThumbnailRequest, rsp *v0proto.GetThumbnailResponse) error {
timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
us := v * 1000_000
i.metrics.Latency.WithLabelValues().Observe(us)

View File

@@ -22,7 +22,7 @@ type logging struct {
}
// GetThumbnail implements the ThumbnailServiceHandler interface.
func (l logging) GetThumbnail(ctx context.Context, req *v0proto.GetRequest, rsp *v0proto.GetResponse) error {
func (l logging) GetThumbnail(ctx context.Context, req *v0proto.GetThumbnailRequest, rsp *v0proto.GetThumbnailResponse) error {
start := time.Now()
err := l.next.GetThumbnail(ctx, req, rsp)

View File

@@ -1,6 +1,7 @@
package svc
import (
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
"net/http"
"github.com/owncloud/ocis/ocis-pkg/log"
@@ -19,6 +20,8 @@ type Options struct {
Middleware []func(http.Handler) http.Handler
ThumbnailStorage storage.Storage
ImageSource imgsource.Source
CS3Source imgsource.Source
CS3Client gateway.GatewayAPIClient
}
// newOptions initializes the available default options.
@@ -66,3 +69,15 @@ func ThumbnailSource(val imgsource.Source) Option {
o.ImageSource = val
}
}
func CS3Source(val imgsource.Source) Option {
return func(o *Options) {
o.CS3Source = val
}
}
func CS3Client(c gateway.GatewayAPIClient) Option {
return func(o *Options) {
o.CS3Client = c
}
}

View File

@@ -2,13 +2,21 @@ package svc
import (
"context"
"image"
merrors "github.com/asim/go-micro/v3/errors"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/pkg/token"
"github.com/owncloud/ocis/ocis-pkg/log"
v0proto "github.com/owncloud/ocis/thumbnails/pkg/proto/v0"
"github.com/owncloud/ocis/thumbnails/pkg/thumbnail"
"github.com/owncloud/ocis/thumbnails/pkg/thumbnail/imgsource"
"github.com/pkg/errors"
"google.golang.org/grpc/metadata"
"image"
"net/url"
"path"
"strings"
)
// NewService returns a service implementation for Service.
@@ -26,8 +34,10 @@ func NewService(opts ...Option) v0proto.ThumbnailServiceHandler {
options.ThumbnailStorage,
logger,
),
source: options.ImageSource,
logger: logger,
webdavSource: options.ImageSource,
cs3Source: options.CS3Source,
logger: logger,
cs3Client: options.CS3Client,
}
return svc
@@ -35,57 +45,186 @@ func NewService(opts ...Option) v0proto.ThumbnailServiceHandler {
// Thumbnail implements the GRPC handler.
type Thumbnail struct {
serviceID string
manager thumbnail.Manager
source imgsource.Source
logger log.Logger
serviceID string
manager thumbnail.Manager
webdavSource imgsource.Source
cs3Source imgsource.Source
logger log.Logger
cs3Client gateway.GatewayAPIClient
}
// GetThumbnail retrieves a thumbnail for an image
func (g Thumbnail) GetThumbnail(ctx context.Context, req *v0proto.GetRequest, rsp *v0proto.GetResponse) error {
encoder := thumbnail.EncoderForType(req.Filetype.String())
func (g Thumbnail) GetThumbnail(ctx context.Context, req *v0proto.GetThumbnailRequest, rsp *v0proto.GetThumbnailResponse) error {
_, ok := v0proto.GetThumbnailRequest_ThumbnailType_value[req.ThumbnailType.String()]
if !ok {
g.logger.Debug().Str("thumbnail_type", req.ThumbnailType.String()).Msg("unsupported thumbnail type")
return nil
}
encoder := thumbnail.EncoderForType(req.ThumbnailType.String())
if encoder == nil {
g.logger.Debug().Str("filetype", req.Filetype.String()).Msg("unsupported filetype")
g.logger.Debug().Str("thumbnail_type", req.ThumbnailType.String()).Msg("unsupported thumbnail type")
return nil
}
auth := req.Authorization
if auth == "" {
return merrors.BadRequest(g.serviceID, "authorization is missing")
var thumb []byte
var err error
switch {
case req.GetWebdavSource() != nil:
thumb, err = g.handleWebdavSource(ctx, req, encoder)
case req.GetCs3Source() != nil:
thumb, err = g.handleCS3Source(ctx, req, encoder)
default:
g.logger.Error().Msg("no image source provided")
return merrors.BadRequest(g.serviceID, "image source is missing")
}
username := req.Username
if username == "" {
return merrors.BadRequest(g.serviceID, "username missing in request")
if err != nil {
return err
}
rsp.Thumbnail = thumb
rsp.Mimetype = encoder.MimeType()
return nil
}
func (g Thumbnail) handleCS3Source(ctx context.Context, req *v0proto.GetThumbnailRequest, encoder thumbnail.Encoder) ([]byte, error) {
src := req.GetCs3Source()
sRes, err := g.stat(src.Path, src.Authorization)
if err != nil {
return nil, err
}
tr := thumbnail.Request{
Resolution: image.Rect(0, 0, int(req.Width), int(req.Height)),
Encoder: encoder,
ETag: req.Etag,
Username: username,
Checksum: sRes.GetInfo().GetChecksum().GetSum(),
}
thumbnail := g.manager.GetStored(tr)
if thumbnail != nil {
rsp.Thumbnail = thumbnail
rsp.Mimetype = tr.Encoder.MimeType()
return nil
thumb, ok := g.manager.Get(tr)
if ok {
return thumb, nil
}
sCtx := imgsource.ContextSetAuthorization(ctx, auth)
img, err := g.source.Get(sCtx, req.Filepath)
ctx = imgsource.ContextSetAuthorization(ctx, src.Authorization)
img, err := g.cs3Source.Get(ctx, src.Path)
if err != nil {
return merrors.InternalServerError(g.serviceID, "could not get image from source: %v", err.Error())
return nil, merrors.InternalServerError(g.serviceID, "could not get image from source: %s", err.Error())
}
if img == nil {
return merrors.InternalServerError(g.serviceID, "could not get image from source")
return nil, merrors.InternalServerError(g.serviceID, "could not get image from source")
}
thumbnail, err = g.manager.Get(tr, img)
if err != nil {
return err
if thumb, err = g.manager.Generate(tr, img); err != nil {
return nil, err
}
rsp.Thumbnail = thumbnail
rsp.Mimetype = tr.Encoder.MimeType()
return nil
return thumb, nil
}
func (g Thumbnail) handleWebdavSource(ctx context.Context, req *v0proto.GetThumbnailRequest, encoder thumbnail.Encoder) ([]byte, error) {
src := req.GetWebdavSource()
imgURL, err := url.Parse(src.Url)
if err != nil {
return nil, errors.Wrap(err, "source url is invalid")
}
var auth, statPath string
if src.IsPublicLink {
q := imgURL.Query()
var rsp *gateway.AuthenticateResponse
if q.Get("signature") != "" && q.Get("expiration") != "" {
// Handle pre-signed public links
sig := q.Get("signature")
exp := q.Get("expiration")
rsp, err = g.cs3Client.Authenticate(ctx, &gateway.AuthenticateRequest{
Type: "publicshares",
ClientId: src.PublicLinkToken,
ClientSecret: strings.Join([]string{"signature", sig, exp}, "|"),
})
} else {
rsp, err = g.cs3Client.Authenticate(ctx, &gateway.AuthenticateRequest{
Type: "publicshares",
ClientId: src.PublicLinkToken,
// We pass an empty password because we expect non pre-signed public links
// to not be password protected
ClientSecret: "password|",
})
}
if err != nil {
return nil, merrors.InternalServerError(g.serviceID, "could not authenticate: %s", err.Error())
}
auth = rsp.Token
statPath = path.Join("/public", src.PublicLinkToken, req.Filepath)
} else {
auth = src.RevaAuthorization
statPath = path.Join("/home", req.Filepath)
}
sRes, err := g.stat(statPath, auth)
if err != nil {
return nil, err
}
tr := thumbnail.Request{
Resolution: image.Rect(0, 0, int(req.Width), int(req.Height)),
Encoder: encoder,
Checksum: sRes.GetInfo().GetChecksum().GetSum(),
}
thumb, ok := g.manager.Get(tr)
if ok {
return thumb, nil
}
if src.WebdavAuthorization != "" {
ctx = imgsource.ContextSetAuthorization(ctx, src.WebdavAuthorization)
}
imgURL.RawQuery = ""
img, err := g.webdavSource.Get(ctx, imgURL.String())
if err != nil {
return nil, merrors.InternalServerError(g.serviceID, "could not get image from source: %s", err.Error())
}
if img == nil {
return nil, merrors.InternalServerError(g.serviceID, "could not get image from source")
}
if thumb, err = g.manager.Generate(tr, img); err != nil {
return nil, err
}
return thumb, nil
}
func (g Thumbnail) stat(path, auth string) (*provider.StatResponse, error) {
ctx := metadata.AppendToOutgoingContext(context.Background(), token.TokenHeader, auth)
req := &provider.StatRequest{
Ref: &provider.Reference{
Spec: &provider.Reference_Path{Path: path},
},
}
rsp, err := g.cs3Client.Stat(ctx, req)
if err != nil {
g.logger.Error().Err(err).Str("path", path).Msg("could not stat file")
return nil, merrors.InternalServerError(g.serviceID, "could not stat file: %s", err.Error())
}
if rsp.Status.Code != rpc.Code_CODE_OK {
switch rsp.Status.Code {
case rpc.Code_CODE_NOT_FOUND:
return nil, merrors.NotFound(g.serviceID, "could not stat file: %s", rsp.Status.Message)
default:
g.logger.Error().Str("status_message", rsp.Status.Message).Str("path", path).Msg("could not stat file")
return nil, merrors.InternalServerError(g.serviceID, "could not stat file: %s", rsp.Status.Message)
}
}
if rsp.Info.Type != provider.ResourceType_RESOURCE_TYPE_FILE {
return nil, merrors.BadRequest(g.serviceID, "Unsupported file type")
}
if rsp.Info.GetChecksum().GetSum() == "" {
g.logger.Error().Msg("resource info is missing checksum")
return nil, merrors.NotFound(g.serviceID, "resource info is missing a checksum")
}
if !thumbnail.IsMimeTypeSupported(rsp.Info.MimeType) {
return nil, merrors.NotFound(g.serviceID, "Unsupported file type")
}
return rsp, nil
}

View File

@@ -19,14 +19,13 @@ type tracing struct {
}
// GetThumbnail implements the ThumbnailServiceHandler interface.
func (t tracing) GetThumbnail(ctx context.Context, req *v0proto.GetRequest, rsp *v0proto.GetResponse) error {
func (t tracing) GetThumbnail(ctx context.Context, req *v0proto.GetThumbnailRequest, rsp *v0proto.GetThumbnailResponse) error {
ctx, span := trace.StartSpan(ctx, "Thumbnails.GetThumbnail")
defer span.End()
span.Annotate([]trace.Attribute{
trace.StringAttribute("filepath", req.Filepath),
trace.StringAttribute("filetype", req.Filetype.String()),
trace.StringAttribute("etag", req.Etag),
trace.StringAttribute("thumbnail_type", req.ThumbnailType.String()),
trace.Int64Attribute("width", int64(req.Width)),
trace.Int64Attribute("height", int64(req.Height)),
}, "Execute Thumbnails.GetThumbnail handler")

View File

@@ -0,0 +1,81 @@
package imgsource
import (
"context"
"crypto/tls"
"fmt"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/pkg/rhttp"
"github.com/cs3org/reva/pkg/token"
"github.com/pkg/errors"
"google.golang.org/grpc/metadata"
"image"
"net/http"
)
type CS3 struct {
client gateway.GatewayAPIClient
}
func NewCS3Source(c gateway.GatewayAPIClient) CS3 {
return CS3{
client: c,
}
}
func (s CS3) Get(ctx context.Context, path string) (image.Image, error) {
auth, ok := ContextGetAuthorization(ctx)
if !ok {
return nil, errors.New("cs3source: authorization missing")
}
ctx = metadata.AppendToOutgoingContext(context.Background(), token.TokenHeader, auth)
rsp, err := s.client.InitiateFileDownload(ctx, &provider.InitiateFileDownloadRequest{
Ref: &provider.Reference{
Spec: &provider.Reference_Path{
Path: path,
},
},
})
if err != nil {
return nil, err
}
if rsp.Status.Code != rpc.Code_CODE_OK {
return nil, fmt.Errorf("could not load image: %s", rsp.Status.Message)
}
var ep, tk string
for _, p := range rsp.Protocols {
if p.Protocol == "simple" {
ep, tk = p.DownloadEndpoint, p.Token
}
}
httpReq, err := rhttp.NewRequest(ctx, "GET", ep, nil)
if err != nil {
return nil, err
}
httpReq.Header.Set(token.TokenHeader, auth)
httpReq.Header.Set("X-REVA-TRANSFER", tk)
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec
client := &http.Client{}
resp, err := client.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close() //nolint:errcheck
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("could not get the image \"%s\". Request returned with statuscode %d ", path, resp.StatusCode)
}
img, _, err := image.Decode(resp.Body)
if err != nil {
return nil, errors.Wrapf(err, `could not decode the image "%s"`, path)
}
return img, nil
}

View File

@@ -4,63 +4,54 @@ import (
"context"
"crypto/tls"
"fmt"
"github.com/owncloud/ocis/thumbnails/pkg/config"
"github.com/pkg/errors"
"image"
_ "image/gif" // Import the gif package so that image.Decode can understand gifs
_ "image/jpeg" // Import the jpeg package so that image.Decode can understand jpegs
_ "image/png" // Import the png package so that image.Decode can understand pngs
"net/http"
"net/url"
"path"
"github.com/owncloud/ocis/thumbnails/pkg/config"
"github.com/pkg/errors"
)
// NewWebDavSource creates a new webdav instance.
func NewWebDavSource(cfg config.WebDavSource) WebDav {
func NewWebDavSource(cfg config.Thumbnail) WebDav {
return WebDav{
baseURL: cfg.BaseURL,
insecure: cfg.Insecure,
insecure: cfg.WebdavAllowInsecure,
}
}
// WebDav implements the Source interface for webdav services
type WebDav struct {
baseURL string
insecure bool
}
// Get downloads the file from a webdav service
func (s WebDav) Get(ctx context.Context, file string) (image.Image, error) {
u, _ := url.Parse(s.baseURL)
u.Path = path.Join(u.Path, file)
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
func (s WebDav) Get(ctx context.Context, url string) (image.Image, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, errors.Wrapf(err, `could not get the image "%s"`, file)
return nil, errors.Wrapf(err, `could not get the image "%s"`, url)
}
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: s.insecure} //nolint:gosec
auth, ok := ContextGetAuthorization(ctx)
if !ok {
return nil, fmt.Errorf("could not get image \"%s\" error: authorization is missing", file)
if auth, ok := ContextGetAuthorization(ctx); ok {
req.Header.Add("Authorization", auth)
}
req.Header.Add("Authorization", auth)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, errors.Wrapf(err, `could not get the image "%s"`, file)
return nil, errors.Wrapf(err, `could not get the image "%s"`, url)
}
defer resp.Body.Close() //nolint:errcheck
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("could not get the image \"%s\". Request returned with statuscode %d ", file, resp.StatusCode)
return nil, fmt.Errorf("could not get the image \"%s\". Request returned with statuscode %d ", url, resp.StatusCode)
}
img, _, err := image.Decode(resp.Body)
if err != nil {
return nil, errors.Wrapf(err, `could not decode the image "%s"`, file)
return nil, errors.Wrapf(err, `could not decode the image "%s"`, url)
}
return img, nil
}

View File

@@ -93,15 +93,6 @@ func (rs Resolutions) ClosestMatch(requested image.Rectangle, sourceSize image.R
return match
}
func mapRatio(given image.Rectangle, other image.Rectangle) image.Rectangle {
isLandscape := given.Dx() > given.Dy()
ratio := float64(given.Dx()) / float64(given.Dy())
if isLandscape {
return image.Rect(0, 0, other.Dx(), int(float64(other.Dx())/ratio))
}
return image.Rect(0, 0, int(float64(other.Dy())*ratio), other.Dy())
}
func dimensionLength(rect image.Rectangle, isLandscape bool) int {
if isLandscape {
return rect.Dx()

View File

@@ -119,20 +119,3 @@ func TestParseResolution(t *testing.T) {
t.Errorf("Expected resolution %s got %s", rStr, r.String())
}
}
func TestMapRatio(t *testing.T) {
testData := [][]image.Rectangle{
{image.Rect(0, 0, 1920, 1080), image.Rect(0, 0, 32, 32), image.Rect(0, 0, 32, 18)},
{image.Rect(0, 0, 1080, 1920), image.Rect(0, 0, 32, 32), image.Rect(0, 0, 18, 32)},
{image.Rect(0, 0, 1024, 735), image.Rect(0, 0, 32, 32), image.Rect(0, 0, 32, 22)},
}
for _, row := range testData {
given := row[0]
other := row[1]
expected := row[2]
mapped := mapRatio(given, other)
if mapped.Dx() != expected.Dx() || mapped.Dy() != expected.Dy() {
t.Errorf("Expected %dx%d got %dx%d", expected.Dx(), expected.Dy(), mapped.Dx(), mapped.Dy())
}
}
}

View File

@@ -1,26 +1,19 @@
package storage
import (
"crypto/sha256"
"encoding/hex"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/owncloud/ocis/thumbnails/pkg/config"
"github.com/pkg/errors"
"os"
"path/filepath"
"strconv"
)
const (
usersDir = "users"
filesDir = "files"
)
// NewFileSystemStorage creates a new instanz of FileSystem
// NewFileSystemStorage creates a new instance of FileSystem
func NewFileSystemStorage(cfg config.FileSystemStorage, logger log.Logger) *FileSystem {
return &FileSystem{
root: cfg.RootDirectory,
@@ -32,117 +25,56 @@ func NewFileSystemStorage(cfg config.FileSystemStorage, logger log.Logger) *File
type FileSystem struct {
root string
logger log.Logger
mux sync.Mutex
}
// Get loads the image from the file system.
func (s *FileSystem) Get(username string, key string) []byte {
userDir := s.userDir(username)
img := filepath.Join(userDir, key)
content, err := ioutil.ReadFile(img)
func (s *FileSystem) Get(key string) ([]byte, bool) {
img := filepath.Join(s.root, filesDir, key)
content, err := os.ReadFile(img)
if err != nil {
s.logger.Debug().Str("err", err.Error()).Str("key", key).Msg("could not load thumbnail from store")
return nil
if !os.IsNotExist(err) {
s.logger.Debug().Str("err", err.Error()).Str("key", key).Msg("could not load thumbnail from store")
}
return nil, false
}
return content
return content, true
}
// Set writes the image to the file system.
func (s *FileSystem) Set(username string, key string, img []byte) error {
_, err := s.storeImage(key, img)
if err != nil {
return errors.Wrap(err, "could not store image")
}
userDir, err := s.createUserDir(username)
if err != nil {
return err
}
return s.linkImageToUserDir(key, userDir)
}
// BuildKey generate the unique key for a thumbnail.
// The key is structure as follows:
//
// <first two letters of etag>/<next two letters of etag>/<rest of etag>/<width>x<height>.<filetype>
//
// e.g. 97/9f/4c8db98f7b82e768ef478d3c8612/500x300.png
//
// The key also represents the path to the thumbnail in the filesystem under the configured root directory.
func (s *FileSystem) BuildKey(r Request) string {
etag := r.ETag
filetype := r.Types[0]
filename := strconv.Itoa(r.Resolution.Dx()) + "x" + strconv.Itoa(r.Resolution.Dy()) + "." + filetype
return filepath.Join(etag[:2], etag[2:4], etag[4:], filename)
}
func (s *FileSystem) rootDir(key string) string {
p := strings.Split(key, string(os.PathSeparator))
return p[0]
}
func (s *FileSystem) storeImage(key string, img []byte) (string, error) {
s.mux.Lock()
defer s.mux.Unlock()
func (s *FileSystem) Put(key string, img []byte) error {
imgPath := filepath.Join(s.root, filesDir, key)
dir := filepath.Dir(imgPath)
if err := os.MkdirAll(dir, 0700); err != nil {
return "", errors.Wrapf(err, "error while creating directory %s", dir)
return errors.Wrapf(err, "error while creating directory %s", dir)
}
if _, err := os.Stat(imgPath); os.IsNotExist(err) {
f, err := os.Create(imgPath)
if err != nil {
return "", errors.Wrapf(err, "could not create file \"%s\"", key)
return errors.Wrapf(err, "could not create file \"%s\"", key)
}
defer f.Close()
_, err = f.Write(img)
if err != nil {
return "", errors.Wrapf(err, "could not write to file \"%s\"", key)
if _, err = f.Write(img); err != nil {
return errors.Wrapf(err, "could not write to file \"%s\"", key)
}
}
return imgPath, nil
}
// userDir returns the path to the user directory.
// The username is hashed before appending it on the path to prevent bugs caused by invalid folder names.
// Also the hash is then splitted up in three parts that results in a path which looks as follows:
// <filestorage-root>/users/<3 characters>/<3 characters>/<48 characters>/
// This will balance the folders in setups with many users.
func (s *FileSystem) userDir(username string) string {
hash := sha256.New224()
if _, err := hash.Write([]byte(username)); err != nil {
s.logger.Fatal().Err(err).Msg("failed to create hash")
}
unHash := hex.EncodeToString(hash.Sum(nil)) // 224 Bits or 224 / 4 = 56 characters.
return filepath.Join(s.root, usersDir, unHash[:3], unHash[3:6], unHash[6:])
}
func (s *FileSystem) createUserDir(username string) (string, error) {
userDir := s.userDir(username)
if err := os.MkdirAll(userDir, 0700); err != nil {
return "", errors.Wrapf(err, "could not create userDir: %s", userDir)
}
return userDir, nil
}
// linkImageToUserDir links the stored images to the user directory.
// The goal is to minimize disk usage by linking to the images if they already exist and avoid file duplication.
func (s *FileSystem) linkImageToUserDir(key string, userDir string) error {
imgRootDir := s.rootDir(key)
s.mux.Lock()
defer s.mux.Unlock()
err := os.Symlink(filepath.Join(s.root, filesDir, imgRootDir), filepath.Join(userDir, imgRootDir))
if err != nil {
if !os.IsExist(err) {
return errors.Wrap(err, "could not link image to userdir")
}
}
return nil
}
// BuildKey generate the unique key for a thumbnail.
// The key is structure as follows:
//
// <first two letters of checksum>/<next two letters of checksum>/<rest of checksum>/<width>x<height>.<filetype>
//
// e.g. 97/9f/4c8db98f7b82e768ef478d3c8612/500x300.png
//
// The key also represents the path to the thumbnail in the filesystem under the configured root directory.
func (s *FileSystem) BuildKey(r Request) string {
checksum := r.Checksum
filetype := r.Types[0]
filename := strconv.Itoa(r.Resolution.Dx()) + "x" + strconv.Itoa(r.Resolution.Dy()) + "." + filetype
return filepath.Join(checksum[:2], checksum[2:4], checksum[4:], filename)
}

View File

@@ -7,38 +7,31 @@ import (
// NewInMemoryStorage creates a new InMemory instance.
func NewInMemoryStorage() InMemory {
return InMemory{
store: make(map[string]map[string][]byte),
store: make(map[string][]byte),
}
}
// InMemory represents an in memory storage for thumbnails
// Can be used during development
type InMemory struct {
store map[string]map[string][]byte
store map[string][]byte
}
// Get loads the thumbnail from memory.
func (s InMemory) Get(username string, key string) []byte {
userImages := s.store[username]
if userImages == nil {
return nil
}
return s.store[username][key]
func (s InMemory) Get(key string) ([]byte, bool) {
return s.store[key], true
}
// Set stores the thumbnail in memory.
func (s InMemory) Set(username string, key string, thumbnail []byte) error {
if _, ok := s.store[username]; !ok {
s.store[username] = make(map[string][]byte)
}
s.store[username][key] = thumbnail
func (s InMemory) Put(key string, thumbnail []byte) error {
s.store[key] = thumbnail
return nil
}
// BuildKey generates a unique key to store and retrieve the thumbnail.
func (s InMemory) BuildKey(r Request) string {
parts := []string{
r.ETag,
r.Checksum,
r.Resolution.String(),
strings.Join(r.Types, ","),
}

View File

@@ -6,14 +6,20 @@ import (
// Request combines different attributes needed for storage operations.
type Request struct {
ETag string
// The checksum of the source file
// Will be used to determine if a thumbnail exists
Checksum string
// Types provided by the encoder.
// Contains the mimetypes of the thumbnail.
// In case of jpg/jpeg it will contain both.
Types []string
// The resolution of the thumbnail
Resolution image.Rectangle
}
// Storage defines the interface for a thumbnail store.
type Storage interface {
Get(string, string) []byte
Set(string, string, []byte) error
Get(string) ([]byte, bool)
Put(string, []byte) error
BuildKey(Request) string
}

View File

@@ -2,28 +2,36 @@ package thumbnail
import (
"bytes"
"image"
"github.com/disintegration/imaging"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/owncloud/ocis/thumbnails/pkg/thumbnail/storage"
"golang.org/x/image/draw"
"image"
"strings"
)
var (
SupportedMimeTypes = [...]string{
"image/png",
"image/jpg",
"image/jpeg",
"image/gif",
}
)
// Request bundles information needed to generate a thumbnail for afile
type Request struct {
Resolution image.Rectangle
Encoder Encoder
ETag string
Username string
Checksum string
}
// Manager is responsible for generating thumbnails
type Manager interface {
// Get will return a thumbnail for a file
Get(Request, image.Image) ([]byte, error)
// GetStored loads the thumbnail from the storage.
// Generate will return a thumbnail for a file
Generate(Request, image.Image) ([]byte, error)
// Get loads the thumbnail from the storage.
// It will return nil if no image is stored for the given context.
GetStored(Request) []byte
Get(Request) ([]byte, bool)
}
// NewSimpleManager creates a new instance of SimpleManager
@@ -42,46 +50,50 @@ type SimpleManager struct {
resolutions Resolutions
}
// Get implements the Get Method of Manager
func (s SimpleManager) Get(r Request, img image.Image) ([]byte, error) {
// Generate creates a thumbnail and stores it.
// The created thumbnail is also being returned.
func (s SimpleManager) Generate(r Request, img image.Image) ([]byte, error) {
match := s.resolutions.ClosestMatch(r.Resolution, img.Bounds())
thumbnail := s.generate(match, img)
key := s.storage.BuildKey(mapToStorageRequest(r))
buf := new(bytes.Buffer)
err := r.Encoder.Encode(buf, thumbnail)
dst := new(bytes.Buffer)
err := r.Encoder.Encode(dst, thumbnail)
if err != nil {
return nil, err
}
bytes := buf.Bytes()
err = s.storage.Set(r.Username, key, bytes)
k := s.storage.BuildKey(mapToStorageRequest(r))
err = s.storage.Put(k, dst.Bytes())
if err != nil {
s.logger.Warn().Err(err).Msg("could not store thumbnail")
}
return bytes, nil
return dst.Bytes(), nil
}
// GetStored tries to get the stored thumbnail and return it.
// Get tries to get the stored thumbnail and return it.
// If there is no cached thumbnail it will return nil
func (s SimpleManager) GetStored(r Request) []byte {
key := s.storage.BuildKey(mapToStorageRequest(r))
stored := s.storage.Get(r.Username, key)
return stored
func (s SimpleManager) Get(r Request) ([]byte, bool) {
k := s.storage.BuildKey(mapToStorageRequest(r))
return s.storage.Get(k)
}
func (s SimpleManager) generate(r image.Rectangle, img image.Image) image.Image {
targetResolution := mapRatio(img.Bounds(), r)
thumbnail := image.NewRGBA(targetResolution)
draw.ApproxBiLinear.Scale(thumbnail, targetResolution, img, img.Bounds(), draw.Over, nil)
return thumbnail
return imaging.Thumbnail(img, r.Dx(), r.Dy(), imaging.Lanczos)
}
func mapToStorageRequest(r Request) storage.Request {
sR := storage.Request{
ETag: r.ETag,
return storage.Request{
Checksum: r.Checksum,
Resolution: r.Resolution,
Types: r.Encoder.Types(),
}
return sR
}
func IsMimeTypeSupported(m string) bool {
for _, mt := range SupportedMimeTypes {
if strings.EqualFold(mt, m) {
return true
}
}
return false
}

View File

@@ -33,7 +33,7 @@ func BenchmarkGet(b *testing.B) {
res, _ := ParseResolution("32x32")
req := Request{
Resolution: res,
ETag: "1872ade88f3013edeb33decd74a4f947",
Checksum: "1872ade88f3013edeb33decd74a4f947",
}
cwd, _ := os.Getwd()
p := filepath.Join(cwd, "../../testdata/oc.png")
@@ -42,6 +42,6 @@ func BenchmarkGet(b *testing.B) {
img, ext, _ := image.Decode(f)
req.Encoder = EncoderForType(ext)
for i := 0; i < b.N; i++ {
_, _ = sut.Get(req, img)
_, _ = sut.Generate(req, img)
}
}

View File

@@ -8,6 +8,7 @@ require (
contrib.go.opencensus.io/exporter/zipkin v0.1.2
github.com/asim/go-micro/v3 v3.5.1-0.20210217182006-0f0ace1a44a9
github.com/go-chi/chi v4.1.2+incompatible
github.com/go-chi/render v1.0.1
github.com/micro/cli/v2 v2.1.2
github.com/oklog/run v1.1.0
github.com/olekukonko/tablewriter v0.0.5

View File

@@ -319,6 +319,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dnaeon/go-vcr v0.0.0-20180814043457-aafff18a5cc2/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/dnsimple/dnsimple-go v0.30.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
@@ -378,6 +380,7 @@ github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkPro
github.com/go-bindata/go-bindata v3.1.1+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
@@ -1659,8 +1662,9 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=

View File

@@ -48,6 +48,7 @@ type Config struct {
HTTP HTTP
Tracing Tracing
Service Service
OcisPublicURL string
Context context.Context
Supervised bool

View File

@@ -0,0 +1,103 @@
package requests
import (
"errors"
"fmt"
"net/http"
"net/url"
"path/filepath"
"strconv"
"strings"
"github.com/go-chi/chi"
)
const (
// DefaultWidth defines the default width of a thumbnail
DefaultWidth = 32
// DefaultHeight defines the default height of a thumbnail
DefaultHeight = 32
)
// ThumbnailRequest combines all parameters provided when requesting a thumbnail
type ThumbnailRequest struct {
// The file path of the source file
Filepath string
// The file name of the source file including the extension
Filename string
// The file extension
Extension string
// The requested width of the thumbnail
Width int32
// The requested height of the thumbnail
Height int32
// In case of a public share the public link token.
PublicLinkToken string
}
// ParseThumbnailRequest extracts all required parameters from a http request.
func ParseThumbnailRequest(r *http.Request) (*ThumbnailRequest, error) {
fp, err := extractFilePath(r)
if err != nil {
return nil, err
}
q := r.URL.Query()
width, height, err := parseDimensions(q)
if err != nil {
return nil, err
}
return &ThumbnailRequest{
Filepath: fp,
Filename: filepath.Base(fp),
Extension: filepath.Ext(fp),
Width: int32(width),
Height: int32(height),
PublicLinkToken: chi.URLParam(r, "token"),
}, nil
}
// the url looks as followed
//
// /remote.php/dav/files/<user>/<filepath>
//
// User and filepath are dynamic and filepath can contain slashes
// So using the URLParam function is not possible.
func extractFilePath(r *http.Request) (string, error) {
user := chi.URLParam(r, "user")
if user != "" {
parts := strings.SplitN(r.URL.Path, user, 2)
return parts[1], nil
}
token := chi.URLParam(r, "token")
if token != "" {
parts := strings.SplitN(r.URL.Path, token, 2)
return parts[1], nil
}
return "", errors.New("could not extract file path")
}
func parseDimensions(q url.Values) (int64, int64, error) {
width, err := parseDimension(q.Get("x"), "width", DefaultWidth)
if err != nil {
return 0, 0, err
}
height, err := parseDimension(q.Get("y"), "height", DefaultHeight)
if err != nil {
return 0, 0, err
}
return width, height, nil
}
func parseDimension(d, name string, defaultValue int64) (int64, error) {
if d == "" {
return defaultValue, nil
}
result, err := strconv.ParseInt(d, 10, 32)
if err != nil || result < 1 {
// The error message doesn't fit but for OC10 API compatibility reasons we have to set this.
return 0, fmt.Errorf("Cannot set %s of 0 or smaller!", name) //nolint:golint
}
return result, nil
}

View File

@@ -1,74 +0,0 @@
package thumbnail
import (
"fmt"
"net/http"
"path/filepath"
"strconv"
"strings"
"github.com/go-chi/chi"
)
const (
// DefaultWidth defines the default width of a thumbnail
DefaultWidth = 32
// DefaultHeight defines the default height of a thumbnail
DefaultHeight = 32
)
// Request combines all parameters provided when requesting a thumbnail
type Request struct {
Filepath string
Extension string
Etag string
Width int
Height int
Authorization string
Username string
}
// NewRequest extracts all required parameters from a http request.
func NewRequest(r *http.Request) (Request, error) {
path := extractFilePath(r)
query := r.URL.Query()
width, err := strconv.Atoi(query.Get("x"))
if err != nil {
width = DefaultWidth
}
height, err := strconv.Atoi(query.Get("y"))
if err != nil {
height = DefaultHeight
}
etag := query.Get("c")
if strings.TrimSpace(etag) == "" {
return Request{}, fmt.Errorf("c (etag) is missing in query")
}
authorization := r.Header.Get("Authorization")
tr := Request{
Filepath: path,
Extension: filepath.Ext(path),
Etag: etag,
Width: width,
Height: height,
Authorization: authorization,
Username: chi.URLParam(r, "user"),
}
return tr, nil
}
// the url looks as followed
//
// /remote.php/dav/files/<user>/<filepath>
//
// User and filepath are dynamic and filepath can contain slashes
// So using the URLParam function is not possible.
func extractFilePath(r *http.Request) string {
user := chi.URLParam(r, "user")
parts := strings.SplitN(r.URL.Path, user, 2)
return parts[1]
}

View File

@@ -141,6 +141,13 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag {
EnvVars: []string{"WEBDAV_HTTP_ROOT"},
Destination: &cfg.HTTP.Root,
},
&cli.StringFlag{
Name: "ocis-public-url",
Value: flags.OverrideDefaultString(cfg.OcisPublicURL, "https://127.0.0.1:9200"),
Usage: "The domain under which oCIS is reachable",
EnvVars: []string{"OCIS_PUBLIC_URL", "OCIS_URL"},
Destination: &cfg.OcisPublicURL,
},
}
}

View File

@@ -1,8 +1,11 @@
package svc
import (
"io"
"encoding/xml"
merrors "github.com/asim/go-micro/v3/errors"
"github.com/go-chi/render"
"net/http"
"path"
"strings"
"github.com/owncloud/ocis/ocis-pkg/log"
@@ -11,7 +14,20 @@ import (
"github.com/go-chi/chi"
thumbnails "github.com/owncloud/ocis/thumbnails/pkg/proto/v0"
"github.com/owncloud/ocis/webdav/pkg/config"
thumbnail "github.com/owncloud/ocis/webdav/pkg/dav/thumbnails"
"github.com/owncloud/ocis/webdav/pkg/dav/requests"
)
const (
TokenHeader = "X-Access-Token"
)
var (
codesEnum = map[int]string{
http.StatusBadRequest: "Sabre\\DAV\\Exception\\BadRequest",
http.StatusUnauthorized: "Sabre\\DAV\\Exception\\NotAuthenticated",
http.StatusNotFound: "Sabre\\DAV\\Exception\\NotFound",
http.StatusMethodNotAllowed: "Sabre\\DAV\\Exception\\MethodNotAllowed",
}
)
// Service defines the extension handlers.
@@ -31,10 +47,13 @@ func NewService(opts ...Option) Service {
config: options.Config,
log: options.Logger,
mux: m,
thumbnailsClient: thumbnails.NewThumbnailService("com.owncloud.api.thumbnails", grpc.DefaultClient),
}
m.Route(options.Config.HTTP.Root, func(r chi.Router) {
r.Get("/remote.php/dav/files/{user}/*", svc.Thumbnail)
r.Get("/remote.php/dav/public-files/{token}/*", svc.PublicThumbnail)
r.Head("/remote.php/dav/public-files/{token}/*", svc.PublicThumbnailHead)
})
return svc
@@ -45,6 +64,7 @@ type Webdav struct {
config *config.Config
log log.Logger
mux *chi.Mux
thumbnailsClient thumbnails.ThumbnailService
}
// ServeHTTP implements the Service interface.
@@ -54,57 +74,210 @@ func (g Webdav) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Thumbnail implements the Service interface.
func (g Webdav) Thumbnail(w http.ResponseWriter, r *http.Request) {
tr, err := thumbnail.NewRequest(r)
tr, err := requests.ParseThumbnailRequest(r)
if err != nil {
g.log.Error().Err(err).Msg("could not create Request")
w.WriteHeader(http.StatusBadRequest)
mustWrite(g.log, w, []byte(err.Error()))
renderError(w, r, errBadRequest(err.Error()))
return
}
c := thumbnails.NewThumbnailService("com.owncloud.api.thumbnails", grpc.DefaultClient)
rsp, err := c.GetThumbnail(r.Context(), &thumbnails.GetRequest{
t := r.Header.Get(TokenHeader)
rsp, err := g.thumbnailsClient.GetThumbnail(r.Context(), &thumbnails.GetThumbnailRequest{
Filepath: strings.TrimLeft(tr.Filepath, "/"),
Filetype: extensionToFiletype(strings.TrimLeft(tr.Extension, ".")),
Etag: tr.Etag,
Width: int32(tr.Width),
Height: int32(tr.Height),
Authorization: tr.Authorization,
Username: tr.Username,
ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")),
Width: tr.Width,
Height: tr.Height,
Source: &thumbnails.GetThumbnailRequest_Cs3Source{
Cs3Source: &thumbnails.CS3Source{
Path: path.Join("/home", tr.Filepath),
Authorization: t,
},
},
})
if err != nil {
g.log.Error().Err(err).Msg("could not get thumbnail")
w.WriteHeader(http.StatusBadRequest)
mustWrite(g.log, w, []byte(err.Error()))
e := merrors.Parse(err.Error())
switch e.Code {
case http.StatusNotFound:
renderError(w, r, errNotFound(notFoundMsg(tr.Filename)))
case http.StatusBadRequest:
renderError(w, r, errBadRequest(err.Error()))
default:
renderError(w, r, errInternalError(err.Error()))
}
return
}
if len(rsp.Thumbnail) == 0 {
w.WriteHeader(http.StatusNotFound)
renderError(w, r, errNotFound(""))
return
}
g.mustRender(w, r, newThumbnailResponse(rsp))
}
func (g Webdav) PublicThumbnail(w http.ResponseWriter, r *http.Request) {
tr, err := requests.ParseThumbnailRequest(r)
if err != nil {
g.log.Error().Err(err).Msg("could not create Request")
renderError(w, r, errBadRequest(err.Error()))
return
}
rsp, err := g.thumbnailsClient.GetThumbnail(r.Context(), &thumbnails.GetThumbnailRequest{
Filepath: strings.TrimLeft(tr.Filepath, "/"),
ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")),
Width: tr.Width,
Height: tr.Height,
Source: &thumbnails.GetThumbnailRequest_WebdavSource{
WebdavSource: &thumbnails.WebdavSource{
Url: g.config.OcisPublicURL + r.URL.RequestURI(),
IsPublicLink: true,
PublicLinkToken: tr.PublicLinkToken,
},
},
})
if err != nil {
g.log.Error().Err(err).Msg("could not get thumbnail")
e := merrors.Parse(err.Error())
switch e.Code {
case http.StatusNotFound:
renderError(w, r, errNotFound(notFoundMsg(tr.Filename)))
case http.StatusBadRequest:
renderError(w, r, errBadRequest(err.Error()))
default:
renderError(w, r, errInternalError(err.Error()))
}
return
}
if len(rsp.Thumbnail) == 0 {
renderError(w, r, errNotFound(""))
return
}
g.mustRender(w, r, newThumbnailResponse(rsp))
}
func (g Webdav) PublicThumbnailHead(w http.ResponseWriter, r *http.Request) {
tr, err := requests.ParseThumbnailRequest(r)
if err != nil {
g.log.Error().Err(err).Msg("could not create Request")
renderError(w, r, errBadRequest(err.Error()))
return
}
rsp, err := g.thumbnailsClient.GetThumbnail(r.Context(), &thumbnails.GetThumbnailRequest{
Filepath: strings.TrimLeft(tr.Filepath, "/"),
ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")),
Width: tr.Width,
Height: tr.Height,
Source: &thumbnails.GetThumbnailRequest_WebdavSource{
WebdavSource: &thumbnails.WebdavSource{
Url: g.config.OcisPublicURL + r.URL.RequestURI(),
IsPublicLink: true,
PublicLinkToken: tr.PublicLinkToken,
},
},
})
if err != nil {
e := merrors.Parse(err.Error())
switch e.Code {
case http.StatusNotFound:
renderError(w, r, errNotFound(notFoundMsg(tr.Filename)))
case http.StatusBadRequest:
g.log.Error().Err(err).Msg("could not get thumbnail")
renderError(w, r, errBadRequest(err.Error()))
default:
g.log.Error().Err(err).Msg("could not get thumbnail")
renderError(w, r, errInternalError(err.Error()))
}
return
}
if len(rsp.Thumbnail) == 0 {
renderError(w, r, errNotFound(""))
return
}
w.Header().Set("Content-Type", rsp.GetMimetype())
w.WriteHeader(http.StatusOK)
mustWrite(g.log, w, rsp.Thumbnail)
}
func extensionToFiletype(ext string) thumbnails.GetRequest_FileType {
ext = strings.ToUpper(ext)
switch ext {
case "JPG", "PNG":
val := thumbnails.GetRequest_FileType_value[ext]
return thumbnails.GetRequest_FileType(val)
case "JPEG", "GIF":
val := thumbnails.GetRequest_FileType_value["JPG"]
return thumbnails.GetRequest_FileType(val)
func extensionToThumbnailType(ext string) thumbnails.GetThumbnailRequest_ThumbnailType {
switch strings.ToUpper(ext) {
case "GIF", "PNG":
return thumbnails.GetThumbnailRequest_PNG
default:
return thumbnails.GetRequest_FileType(-1)
return thumbnails.GetThumbnailRequest_JPG
}
}
func mustWrite(logger log.Logger, w io.Writer, val []byte) {
if _, err := w.Write(val); err != nil {
logger.Error().Err(err).Msg("could not write response")
func (g Webdav) mustRender(w http.ResponseWriter, r *http.Request, renderer render.Renderer) {
if err := render.Render(w, r, renderer); err != nil {
g.log.Err(err).Msg("failed to write response")
}
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_error
type errResponse struct {
HTTPStatusCode int `json:"-" xml:"-"`
XMLName xml.Name `xml:"d:error"`
Xmlnsd string `xml:"xmlns:d,attr"`
Xmlnss string `xml:"xmlns:s,attr"`
Exception string `xml:"s:exception"`
Message string `xml:"s:message"`
InnerXML []byte `xml:",innerxml"`
}
func newErrResponse(statusCode int, msg string) *errResponse {
rsp := &errResponse{
HTTPStatusCode: statusCode,
Xmlnsd: "DAV",
Xmlnss: "http://sabredav.org/ns",
Exception: codesEnum[statusCode],
}
if msg != "" {
rsp.Message = msg
}
return rsp
}
func errInternalError(msg string) *errResponse {
return newErrResponse(http.StatusInternalServerError, msg)
}
func errBadRequest(msg string) *errResponse {
return newErrResponse(http.StatusBadRequest, msg)
}
func errNotFound(msg string) *errResponse {
return newErrResponse(http.StatusNotFound, msg)
}
type thumbnailResponse struct {
contentType string
thumbnail []byte
}
func (t *thumbnailResponse) Render(w http.ResponseWriter, _ *http.Request) error {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", t.contentType)
_, err := w.Write(t.thumbnail)
return err
}
func newThumbnailResponse(rsp *thumbnails.GetThumbnailResponse) *thumbnailResponse {
return &thumbnailResponse{
contentType: rsp.Mimetype,
thumbnail: rsp.Thumbnail,
}
}
func renderError(w http.ResponseWriter, r *http.Request, err *errResponse) {
render.Status(r, err.HTTPStatusCode)
render.XML(w, r, err)
}
func notFoundMsg(name string) string {
return "File with name " + name + " could not be located"
}