Repository: owncloud/ocis
Pull request #4354 (2022-08-08 10:27:12)

[full-ci] [tests-only] Max and Default Quotas
<!--
Thanks for submitting a change to ownCloud!

This is the bug tracker for the oCIS component. Find other components at https://github.com/owncloud/core/blob/master/.github/CONTRIBUTING.md#guidelines

For fixing potential security issues please see https://owncloud.org/security/

To make it possible for us to get your change reviewed and merged please carefully fill out the requested information below.

Please note that any kind of change needs first be submitted to the master branch which holds the next version of oCIS.

Please set the following labels:

- Set label "Status:Needs-Review" for review or "Status:In-Progress" if the PR still has open tasks.
- Assignment: assign to self
- Reviewers: pick at least one
-->

## Description
<!--- Describe your changes in detail -->
We can now define default and maximum quota amounts for personal and project spaces

## Related Issue
<!--- This project only accepts pull requests related to open issues -->
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
<!--- Please link to the issue here: -->
- Fixes <issue_link>

## Motivation and Context
<!--- Why is this change required? What problem does it solve? -->

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->
- test environment:
- test case 1:
- test case 2:
- ...

## Screenshots (if appropriate):

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Technical debt
- [ ] Tests only (no source changes)

## Checklist:
<!-- Tick the checkboxes when done. -->
<!-- Raise documentation ticket in owncloud.github.io/ -->
- [ ] Code changes
- [ ] Unit tests added
- [ ] Acceptance tests added
- [ ] Documentation ticket raised: <link> 


kobergj: @mmattel I added some configuration values in this PR. (Only 3). Could you have a look if my descriptions are sufficient?
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=4354)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=4354&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=4354&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=4354&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=CODE_SMELL)

[![20.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '20.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_coverage&view=list) [20.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=4354)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=4354&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=4354&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=4354&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=CODE_SMELL)

[![11.8%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '11.8%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_coverage&view=list) [11.8% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_duplicated_lines_density&view=list)


mmattel: Technically the description text is ok 👍 
Two questions: pure bytes could be hard to read. Additional values to shorten it like G|M|K ?
What is the base. Is it 1000 or 1024?

kobergj: @mmattel Thanks for input! You are absolutely right. I added possibility to use strings such as `10GB` or `100PiB` and also updated the description accordingly.
Now when using decimal abbreviations (`KB`, `MB`, `GB`, ...) base 1000 will be used and when using binary abbreviations (`KiB`, `MiB`, ...) then the base is 1024

Could you check again please?
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=4354)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=4354&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=4354&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=4354&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=CODE_SMELL)

[![36.3%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/25-16px.png '36.3%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_coverage&view=list) [36.3% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_duplicated_lines_density&view=list)


mmattel: When you sanitize/validate the string, do you check for numbers only + size extension?
Beside the size what does an admin get as error message when you have a string like 1.024GB? (see the dot between the numbers)
kulmann: What's the plan for the API, keep it in bytes or also expect the string format that you apply to the config now?
kobergj: @mmattel There is no sanitization on the string done. It expects to see a number followed by a known abbreviation. If it can't parse the values it will error level log like this: https://github.com/owncloud/ocis/pull/4354/files#diff-fedfe1baf57a1b40bed18423f6f4d1ee0ed8ebf79ddcda8e94c6ec2decd5b210R70

In your case the error would be `unknown unit '.024GB'. Use common abbreviations such as KB, MiB, GB` which is the correct error message from the service point of view.

And yes a quota of 0 means unlimited. I added that to the description.

@kulmann For now I didn't change the API, but since the package is backwards compatible (it assumes bytes if no unit is given) we could easily add that to all endpoints. Would it make web logic simpler if we would do so?
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=4354)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=4354&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=4354&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=4354&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=CODE_SMELL)

[![37.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/25-16px.png '37.7%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_coverage&view=list) [37.7% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=4354)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=4354&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=4354&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=4354&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=4354&resolved=false&types=CODE_SMELL)

[![37.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/25-16px.png '37.7%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_coverage&view=list) [37.7% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=4354&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/go.mod b/go.mod
index 7ad0b82afad..0e1217415af 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
 	github.com/blevesearch/bleve/v2 v2.3.3
 	github.com/coreos/go-oidc/v3 v3.2.0
 	github.com/cs3org/go-cs3apis v0.0.0-20220719130120-361e9f987d64
-	github.com/cs3org/reva/v2 v2.7.3-0.20220808090146-264a9a8caa99
+	github.com/cs3org/reva/v2 v2.7.3-0.20220810074418-45c42bf59913
 	github.com/disintegration/imaging v1.6.2
 	github.com/ggwhite/go-masker v1.0.9
 	github.com/go-chi/chi/v5 v5.0.7
diff --git a/go.sum b/go.sum
index 9d4ae5a8fc6..4d1570824f0 100644
--- a/go.sum
+++ b/go.sum
@@ -290,8 +290,8 @@ github.com/crewjam/saml v0.4.6 h1:XCUFPkQSJLvzyl4cW9OvpWUbRf0gE7VUpU8ZnilbeM4=
 github.com/crewjam/saml v0.4.6/go.mod h1:ZBOXnNPFzB3CgOkRm7Nd6IVdkG+l/wF+0ZXLqD96t1A=
 github.com/cs3org/go-cs3apis v0.0.0-20220719130120-361e9f987d64 h1:cFnankJOCWndnOns4sKRG7yzH61ammK2Am6rEGWCK40=
 github.com/cs3org/go-cs3apis v0.0.0-20220719130120-361e9f987d64/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
-github.com/cs3org/reva/v2 v2.7.3-0.20220808090146-264a9a8caa99 h1:Kz538bWqfXksl13kM3sisongwxr4Ewb/ePj3CQWQaFw=
-github.com/cs3org/reva/v2 v2.7.3-0.20220808090146-264a9a8caa99/go.mod h1:2TY/LuIbnzpIqbL0J7AtVAaVOGw30zuZkWsPIPbW1pU=
+github.com/cs3org/reva/v2 v2.7.3-0.20220810074418-45c42bf59913 h1:/Pzp2PJTz3D0trTfx32OgyPSr5kUhUm3+kuapOVVeys=
+github.com/cs3org/reva/v2 v2.7.3-0.20220810074418-45c42bf59913/go.mod h1:2TY/LuIbnzpIqbL0J7AtVAaVOGw30zuZkWsPIPbW1pU=
 github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8 h1:Z9lwXumT5ACSmJ7WGnFl+OMLLjpz5uR2fyz7dC255FI=
 github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8/go.mod h1:4abs/jPXcmJzYoYGF91JF9Uq9s/KL5n1jvFDix8KcqY=
 github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
diff --git a/ocis-pkg/bytesize/bytesize.go b/ocis-pkg/bytesize/bytesize.go
new file mode 100644
index 00000000000..f13c979a79a
--- /dev/null
+++ b/ocis-pkg/bytesize/bytesize.go
@@ -0,0 +1,90 @@
+package bytesize
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+)
+
+// ByteSize is the size in bytes
+type ByteSize uint64
+
+// List of available byte sizes
+// NOTE: max is exabyte as we convert to uint64
+const (
+	KB ByteSize = 1000
+	MB ByteSize = 1000 * KB
+	GB ByteSize = 1000 * MB
+	TB ByteSize = 1000 * GB
+	PB ByteSize = 1000 * TB
+	EB ByteSize = 1000 * PB
+
+	KiB ByteSize = 1024
+	MiB ByteSize = 1024 * KiB
+	GiB ByteSize = 1024 * MiB
+	TiB ByteSize = 1024 * GiB
+	PiB ByteSize = 1024 * TiB
+	EiB ByteSize = 1024 * PiB
+)
+
+// Parse parses a Bytesize from a string
+func Parse(s string) (ByteSize, error) {
+	sanitized := strings.TrimSpace(s)
+	if !strings.HasSuffix(sanitized, "B") {
+		u, err := strconv.Atoi(sanitized)
+		return ByteSize(u), err
+	}
+
+	var (
+		value int
+		unit  string
+	)
+
+	template := "%d%s"
+	_, err := fmt.Sscanf(sanitized, template, &value, &unit)
+	if err != nil {
+		return 0, err
+	}
+
+	bytes := ByteSize(value)
+	switch unit {
+	case "KB":
+		bytes *= KB
+	case "MB":
+		bytes *= MB
+	case "GB":
+		bytes *= GB
+	case "TB":
+		bytes *= TB
+	case "PB":
+		bytes *= PB
+	case "EB":
+		bytes *= EB
+	case "KiB":
+		bytes *= KiB
+	case "MiB":
+		bytes *= MiB
+	case "GiB":
+		bytes *= GiB
+	case "TiB":
+		bytes *= TiB
+	case "PiB":
+		bytes *= PiB
+	case "EiB":
+		bytes *= EiB
+	default:
+		return 0, fmt.Errorf("unknown unit '%s'. Use common abbreviations such as KB, MiB, GB", unit)
+	}
+
+	return bytes, nil
+}
+
+// Bytes converts the ByteSize to an uint64
+func (b ByteSize) Bytes() uint64 {
+	return uint64(b)
+}
+
+// String converts the ByteSize to a string
+func (b ByteSize) String() string {
+	return strconv.Itoa(int(b))
+}
diff --git a/protogen/gen/ocis/messages/search/v0/search.pb.go b/protogen/gen/ocis/messages/search/v0/search.pb.go
index d50cb7c38ec..36f32d7f41c 100644
--- a/protogen/gen/ocis/messages/search/v0/search.pb.go
+++ b/protogen/gen/ocis/messages/search/v0/search.pb.go
@@ -154,6 +154,7 @@ type Entity struct {
 	Permissions      string                 `protobuf:"bytes,8,opt,name=permissions,proto3" json:"permissions,omitempty"`
 	Type             uint64                 `protobuf:"varint,9,opt,name=type,proto3" json:"type,omitempty"`
 	Deleted          bool                   `protobuf:"varint,10,opt,name=deleted,proto3" json:"deleted,omitempty"`
+	Tags             []string               `protobuf:"bytes,11,rep,name=tags,proto3" json:"tags,omitempty"`
 }
 
 func (x *Entity) Reset() {
@@ -258,6 +259,13 @@ func (x *Entity) GetDeleted() bool {
 	return false
 }
 
+func (x *Entity) GetTags() []string {
+	if x != nil {
+		return x.Tags
+	}
+	return nil
+}
+
 type Match struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -336,7 +344,7 @@ var file_ocis_messages_search_v0_search_proto_rawDesc = []byte{
 	0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76,
 	0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x52, 0x0a, 0x72, 0x65,
 	0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0xe6, 0x02, 0x0a,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0xfa, 0x02, 0x0a,
 	0x06, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x34, 0x0a, 0x03, 0x72, 0x65, 0x66, 0x18, 0x01,
 	0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73,
 	0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x52,
@@ -359,17 +367,18 @@ var file_ocis_messages_search_v0_search_proto_rawDesc = []byte{
 	0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18,
 	0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x64,
 	0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x64, 0x65,
-	0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0x56, 0x0a, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x37,
-	0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f,
-	0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73,
-	0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52,
-	0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x42, 0x42, 0x5a,
-	0x40, 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, 0x76, 0x32, 0x2f, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x6d,
-	0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x76,
-	0x30, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x0b, 0x20,
+	0x03, 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x22, 0x56, 0x0a, 0x05, 0x4d, 0x61, 0x74,
+	0x63, 0x68, 0x12, 0x37, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
+	0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x45, 0x6e, 0x74,
+	0x69, 0x74, 0x79, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x73,
+	0x63, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72,
+	0x65, 0x42, 0x42, 0x5a, 0x40, 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, 0x76, 0x32,
+	0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x63,
+	0x69, 0x73, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2f, 0x73, 0x65, 0x61, 0x72,
+	0x63, 0x68, 0x2f, 0x76, 0x30, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/protogen/gen/ocis/services/search/v0/search.swagger.json b/protogen/gen/ocis/services/search/v0/search.swagger.json
index 5d0f8bd4dd7..4201f601f40 100644
--- a/protogen/gen/ocis/services/search/v0/search.swagger.json
+++ b/protogen/gen/ocis/services/search/v0/search.swagger.json
@@ -192,6 +192,12 @@
         },
         "deleted": {
           "type": "boolean"
+        },
+        "tags": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
         }
       }
     },
diff --git a/protogen/gen/ocis/services/store/v0/store.pb.micro.go b/protogen/gen/ocis/services/store/v0/store.pb.micro.go
index ed25b475640..53121345666 100644
--- a/protogen/gen/ocis/services/store/v0/store.pb.micro.go
+++ b/protogen/gen/ocis/services/store/v0/store.pb.micro.go
@@ -104,6 +104,7 @@ type Store_ListService interface {
 	Context() context.Context
 	SendMsg(interface{}) error
 	RecvMsg(interface{}) error
+	CloseSend() error
 	Close() error
 	Recv() (*ListResponse, error)
 }
@@ -112,6 +113,10 @@ type storeServiceList struct {
 	stream client.Stream
 }
 
+func (x *storeServiceList) CloseSend() error {
+	return x.stream.CloseSend()
+}
+
 func (x *storeServiceList) Close() error {
 	return x.stream.Close()
 }
diff --git a/protogen/proto/ocis/messages/search/v0/search.proto b/protogen/proto/ocis/messages/search/v0/search.proto
index 3ece6a30800..878b292c988 100644
--- a/protogen/proto/ocis/messages/search/v0/search.proto
+++ b/protogen/proto/ocis/messages/search/v0/search.proto
@@ -7,32 +7,33 @@ import "google/protobuf/timestamp.proto";
 option go_package = "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/search/v0";
 
 message ResourceID {
-	string storage_id = 1;
-	string opaque_id = 2;
-	string space_id = 3;
+  string storage_id = 1;
+  string opaque_id = 2;
+  string space_id = 3;
 }
 
 message Reference {
-	ResourceID resource_id = 1;
-	string path = 2;
+  ResourceID resource_id = 1;
+  string path = 2;
 }
 
 message Entity {
-	Reference ref = 1;
-	ResourceID id = 2;
-	string name = 3;
-	string etag = 4;
-	uint64 size = 5;
-    google.protobuf.Timestamp last_modified_time = 6;
-	string mime_type = 7;
-	string permissions = 8;
-	uint64 type = 9;
-	bool deleted = 10;
+  Reference ref = 1;
+  ResourceID id = 2;
+  string name = 3;
+  string etag = 4;
+  uint64 size = 5;
+  google.protobuf.Timestamp last_modified_time = 6;
+  string mime_type = 7;
+  string permissions = 8;
+  uint64 type = 9;
+  bool deleted = 10;
+  repeated string tags = 11;
 }
 
 message Match {
-	// the matched entity
-	Entity entity = 1;
-	// the match score
-	float score = 2;
+  // the matched entity
+  Entity entity = 1;
+  // the match score
+  float score = 2;
 }
diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go
index 6fea8abc129..2c8d7c6ddaf 100644
--- a/services/graph/pkg/config/config.go
+++ b/services/graph/pkg/config/config.go
@@ -31,7 +31,9 @@ type Config struct {
 type Spaces struct {
 	WebDavBase                      string `yaml:"webdav_base" env:"OCIS_URL;GRAPH_SPACES_WEBDAV_BASE" desc:"The public facing URL of WebDAV."`
 	WebDavPath                      string `yaml:"webdav_path" env:"GRAPH_SPACES_WEBDAV_PATH" desc:"The WebDAV subpath for spaces."`
-	DefaultQuota                    string `yaml:"default_quota" env:"GRAPH_SPACES_DEFAULT_QUOTA" desc:"The default quota in bytes."`
+	DefaultQuota                    string `yaml:"default_quota" env:"GRAPH_SPACES_DEFAULT_QUOTA" desc:"The default quota for project spaces in bytes. Usable common abbreviations: [KB, KiB, GB, GiB, TB, TiB, PB, PiB, EB, EiB], example: 1GB. 0 means unlimited and is the default."`
+	MaxQuotaPersonal                string `yaml:"max_quota_personal" env:"GRAPH_SPACES_MAX_QUOTA_PERSONAL" desc:"The maximum quota for a personal space in bytes. Usable common abbreviations: [KB, KiB, GB, GiB, TB, TiB, PB, PiB, EB, EiB], example: 1GB. 0 means unlimited and is the default."`
+	MaxQuotaProject                 string `yaml:"max_quota_project" env:"GRAPH_SPACES_MAX_QUOTA_PROJECT" desc:"The maximum quota for a project space in bytes. Usable common abbreviations: [KB, KiB, GB, GiB, TB, TiB, PB, PiB, EB, EiB], example: 1GB. 0 means unlimited and is the default."`
 	ExtendedSpacePropertiesCacheTTL int    `yaml:"extended_space_properties_cache_ttl" env:"GRAPH_SPACES_EXTENDED_SPACE_PROPERTIES_CACHE_TTL" desc:"Max TTL in seconds for the spaces property cache."`
 }
 
diff --git a/services/graph/pkg/service/v0/drives.go b/services/graph/pkg/service/v0/drives.go
index dfe702240a7..a85c06ef66c 100644
--- a/services/graph/pkg/service/v0/drives.go
+++ b/services/graph/pkg/service/v0/drives.go
@@ -16,6 +16,7 @@ import (
 	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
 	userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
 	cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
+	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
 	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
@@ -210,22 +211,32 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	var driveType string
+	var (
+		driveType string
+		maxQuota  uint64
+	)
 	if drive.DriveType != nil {
 		driveType = *drive.DriveType
 	}
 	switch driveType {
 	case "", "project":
 		driveType = "project"
+		maxQuota = g.maxQuotaProject
 	default:
 		errorcode.GeneralException.Render(w, r, http.StatusBadRequest, fmt.Sprintf("drives of type %s cannot be created via this api", driveType))
 		return
 	}
 
+	quota := getQuota(drive.Quota, g.defaultQuota)
+	if maxQuota != 0 && maxQuota < quota.QuotaMaxBytes {
+		errorcode.GeneralException.Render(w, r, http.StatusBadRequest, fmt.Sprintf("exceeding maximum drive quota. Max is %d", maxQuota))
+		return
+	}
+
 	csr := storageprovider.CreateStorageSpaceRequest{
 		Type:  driveType,
 		Name:  spaceName,
-		Quota: getQuota(drive.Quota, g.config.Spaces.DefaultQuota),
+		Quota: quota,
 	}
 
 	if drive.Description != nil {
@@ -350,8 +361,50 @@ func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) {
 			errorcode.GeneralException.Render(w, r, http.StatusUnauthorized, "user is not allowed to set the space quota")
 			return
 		}
+
+		maxQuota := g.maxQuotaPersonal
+		if g.maxQuotaPersonal != g.maxQuotaProject {
+			// different quotas set - we need to find out if we are patching a personal or a project space
+			sres, err := client.ListStorageSpaces(r.Context(), &provider.ListStorageSpacesRequest{
+				Filters: []*provider.ListStorageSpacesRequest_Filter{
+					{
+						Type: provider.ListStorageSpacesRequest_Filter_TYPE_ID,
+						Term: &provider.ListStorageSpacesRequest_Filter_Id{
+							Id: &provider.StorageSpaceId{OpaqueId: driveID},
+						},
+					},
+				},
+			})
+			if err != nil {
+				w.WriteHeader(http.StatusInternalServerError)
+				return
+			}
+
+			if len(sres.StorageSpaces) != 1 {
+				errorcode.GeneralException.Render(w, r, http.StatusBadRequest, "drive to patch does not exist")
+				return
+			}
+
+			switch sres.StorageSpaces[0].SpaceType {
+			case "personal":
+				maxQuota = g.maxQuotaPersonal
+			case "project":
+				maxQuota = g.maxQuotaProject
+			default:
+				errorcode.GeneralException.Render(w, r, http.StatusBadRequest, fmt.Sprintf("can't patch drive of type %s", sres.StorageSpaces[0].SpaceType))
+				return
+
+			}
+		}
+
+		quota := uint64(*drive.Quota.Total)
+		if maxQuota != 0 && maxQuota < quota {
+			errorcode.GeneralException.Render(w, r, http.StatusBadRequest, fmt.Sprintf("exceeding maximum drive quota. Max is %d", maxQuota))
+			return
+		}
+
 		updateSpaceRequest.StorageSpace.Quota = &storageprovider.Quota{
-			QuotaMaxBytes: uint64(*drive.Quota.Total),
+			QuotaMaxBytes: quota,
 		}
 	}
 
@@ -677,18 +730,15 @@ func calculateQuotaState(total int64, used int64) (state string) {
 	}
 }
 
-func getQuota(quota *libregraph.Quota, defaultQuota string) *storageprovider.Quota {
+func getQuota(quota *libregraph.Quota, defaultQuota uint64) *storageprovider.Quota {
 	switch {
 	case quota != nil && quota.Total != nil:
 		if q := *quota.Total; q >= 0 {
 			return &storageprovider.Quota{QuotaMaxBytes: uint64(q)}
 		}
 		fallthrough
-	case defaultQuota != "":
-		if q, err := strconv.ParseInt(defaultQuota, 10, 64); err == nil && q >= 0 {
-			return &storageprovider.Quota{QuotaMaxBytes: uint64(q)}
-		}
-		fallthrough
+	case defaultQuota != 0:
+		return &storageprovider.Quota{QuotaMaxBytes: defaultQuota}
 	default:
 		return nil
 	}
diff --git a/services/graph/pkg/service/v0/graph.go b/services/graph/pkg/service/v0/graph.go
index 2f1c98d8dc8..bd751cae719 100644
--- a/services/graph/pkg/service/v0/graph.go
+++ b/services/graph/pkg/service/v0/graph.go
@@ -78,6 +78,9 @@ type Graph struct {
 	roleService          settingssvc.RoleService
 	spacePropertiesCache *ttlcache.Cache
 	eventsPublisher      events.Publisher
+	defaultQuota         uint64
+	maxQuotaPersonal     uint64
+	maxQuotaProject      uint64
 }
 
 // ServeHTTP implements the Service interface.
diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go
index 4f6799339af..d448f0846fa 100644
--- a/services/graph/pkg/service/v0/service.go
+++ b/services/graph/pkg/service/v0/service.go
@@ -13,6 +13,7 @@ import (
 	"github.com/go-chi/chi/v5"
 	"github.com/go-chi/chi/v5/middleware"
 
+	"github.com/owncloud/ocis/v2/ocis-pkg/bytesize"
 	ocisldap "github.com/owncloud/ocis/v2/ocis-pkg/ldap"
 	"github.com/owncloud/ocis/v2/ocis-pkg/roles"
 	"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
@@ -56,6 +57,34 @@ func NewService(opts ...Option) Service {
 
 	m := chi.NewMux()
 	m.Use(options.Middleware...)
+	log := options.Logger
+
+	var (
+		defaultQuota, personalQuota, projectQuota bytesize.ByteSize
+		err                                       error
+	)
+
+	if options.Config.Spaces.DefaultQuota != "" {
+		defaultQuota, err = bytesize.Parse(options.Config.Spaces.DefaultQuota)
+		if err != nil {
+			log.Error().Err(err).Msg("Could not parse default quota - using unlimited")
+		}
+	}
+
+	if options.Config.Spaces.MaxQuotaPersonal != "" {
+		personalQuota, err = bytesize.Parse(options.Config.Spaces.MaxQuotaPersonal)
+		if err != nil {
+			log.Error().Err(err).Msg("Could not parse max personal quota - using unlimited")
+		}
+
+	}
+
+	if options.Config.Spaces.MaxQuotaProject != "" {
+		projectQuota, err = bytesize.Parse(options.Config.Spaces.MaxQuotaProject)
+		if err != nil {
+			log.Error().Err(err).Msg("Could not parse max project quota - using unlimited")
+		}
+	}
 
 	svc := Graph{
 		config:               options.Config,
@@ -63,12 +92,15 @@ func NewService(opts ...Option) Service {
 		logger:               &options.Logger,
 		spacePropertiesCache: ttlcache.NewCache(),
 		eventsPublisher:      options.EventsPublisher,
+		defaultQuota:         defaultQuota.Bytes(),
+		maxQuotaPersonal:     personalQuota.Bytes(),
+		maxQuotaProject:      projectQuota.Bytes(),
 	}
 	if options.GatewayClient == nil {
 		var err error
 		svc.gatewayClient, err = pool.GetGatewayServiceClient(options.Config.Reva.Address)
 		if err != nil {
-			options.Logger.Error().Err(err).Msg("Could not get gateway client")
+			log.Error().Err(err).Msg("Could not get gateway client")
 			return nil
 		}
 	} else {
diff --git a/services/ocdav/pkg/command/server.go b/services/ocdav/pkg/command/server.go
index d98d9e2f911..d8faf9b72b4 100644
--- a/services/ocdav/pkg/command/server.go
+++ b/services/ocdav/pkg/command/server.go
@@ -65,6 +65,7 @@ func Server(cfg *config.Config) *cli.Command {
 					ocdav.VersionString(cfg.Status.VersionString),
 					ocdav.Edition(cfg.Status.Edition),
 					ocdav.MachineAuthAPIKey(cfg.MachineAuthAPIKey),
+					ocdav.Events(cfg.Events.Endpoint, cfg.Events.Cluster),
 					// ocdav.FavoriteManager() // FIXME needs a proper persistence implementation https://github.com/owncloud/ocis/issues/1228
 					// ocdav.LockSystem(), // will default to the CS3 lock system
 					// ocdav.TLSConfig() // tls config for the http server
diff --git a/services/ocdav/pkg/config/config.go b/services/ocdav/pkg/config/config.go
index 3ed98bf3e76..0399b81b8f1 100644
--- a/services/ocdav/pkg/config/config.go
+++ b/services/ocdav/pkg/config/config.go
@@ -36,6 +36,8 @@ type Config struct {
 
 	Context context.Context `yaml:"-"`
 	Status  Status          `yaml:"-"`
+
+	Events Events `yaml:"events"`
 }
 type Tracing struct {
 	Enabled   bool   `yaml:"enabled" env:"OCIS_TRACING_ENABLED;OCDAV_TRACING_ENABLED" desc:"Activates tracing."`
@@ -88,3 +90,9 @@ type Status struct {
 	ProductVersion string
 	Edition        string
 }
+
+// Events combines the configuration options for the event bus.
+type Events struct {
+	Endpoint string `yaml:"endpoint" env:"OCDAV_EVENTS_ENDPOINT" desc:"Endpoint of the event system."`
+	Cluster  string `yaml:"cluster" env:"OCDAV_EVENTS_CLUSTER" desc:"Cluster ID of the event system."`
+}
diff --git a/services/ocdav/pkg/config/defaults/defaultconfig.go b/services/ocdav/pkg/config/defaults/defaultconfig.go
index 23d2c9561e6..a4f0397e9b7 100644
--- a/services/ocdav/pkg/config/defaults/defaultconfig.go
+++ b/services/ocdav/pkg/config/defaults/defaultconfig.go
@@ -52,6 +52,10 @@ func DefaultConfig() *config.Config {
 			ProductName:    "Infinite Scale",
 			Edition:        "Community",
 		},
+		Events: config.Events{
+			Endpoint: "127.0.0.1:9233",
+			Cluster:  "ocis-cluster",
+		},
 	}
 }
 
diff --git a/services/proxy/pkg/command/server.go b/services/proxy/pkg/command/server.go
index 5ce0d1373a7..1b037c8b2d4 100644
--- a/services/proxy/pkg/command/server.go
+++ b/services/proxy/pkg/command/server.go
@@ -227,6 +227,7 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config)
 			middleware.Logger(logger),
 			middleware.TokenManagerConfig(*cfg.TokenManager),
 			middleware.RevaGatewayClient(revaClient),
+			middleware.DefaultSpaceQuota(cfg.DefaultSpaceQuota),
 		),
 		middleware.PublicShareAuth(
 			middleware.Logger(logger),
diff --git a/services/proxy/pkg/config/config.go b/services/proxy/pkg/config/config.go
index 2b9118deabc..aa342dd0d87 100644
--- a/services/proxy/pkg/config/config.go
+++ b/services/proxy/pkg/config/config.go
@@ -33,6 +33,7 @@ type Config struct {
 	EnableBasicAuth       bool            `yaml:"enable_basic_auth" env:"PROXY_ENABLE_BASIC_AUTH" desc:"Set this to true to enable 'basic' (username/password) authentication."`
 	InsecureBackends      bool            `yaml:"insecure_backends" env:"PROXY_INSECURE_BACKENDS" desc:"Disable TLS certificate validation for all HTTP backend connections."`
 	AuthMiddleware        AuthMiddleware  `yaml:"auth_middleware"`
+	DefaultSpaceQuota     string          `yaml:"default_space_quota" env:"OCIS_DEFAULT_SPACE_QUOTA" desc:"The default quota for project spaces in bytes. Usable common abbreviations: [KB, KiB, GB, GiB, TB, TiB, PB, PiB, EB, EiB], example: 1GB. 0 means unlimited and is the default."`
 
 	Context context.Context `yaml:"-" json:"-"`
 }
diff --git a/services/proxy/pkg/middleware/create_home.go b/services/proxy/pkg/middleware/create_home.go
index 5fa30a1a3e2..ee5ecea75b5 100644
--- a/services/proxy/pkg/middleware/create_home.go
+++ b/services/proxy/pkg/middleware/create_home.go
@@ -8,6 +8,8 @@ import (
 	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	revactx "github.com/cs3org/reva/v2/pkg/ctx"
 	"github.com/cs3org/reva/v2/pkg/rgrpc/status"
+	"github.com/cs3org/reva/v2/pkg/utils"
+	"github.com/owncloud/ocis/v2/ocis-pkg/bytesize"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
 	"google.golang.org/grpc/metadata"
 )
@@ -17,11 +19,26 @@ func CreateHome(optionSetters ...Option) func(next http.Handler) http.Handler {
 	options := newOptions(optionSetters...)
 	logger := options.Logger
 
+	// NOTE: reva currently expects bytes to work with - so we need to convert the human readable string
+	// to an uint64 and then convert this to a string again.
+	// It would possibly be simpler to convert bytes sizes on reva side,
+	// but then all byte handling endpoints should be able to do this
+	q, err := bytesize.Parse(options.DefaultSpaceQuota)
+	if err != nil {
+		logger.Error().Str("quota", options.DefaultSpaceQuota).Err(err).Msg("can't parse default quota size - using unlimited")
+	}
+
+	var defaultQuota string
+	if q != 0 {
+		defaultQuota = q.String()
+	}
+
 	return func(next http.Handler) http.Handler {
 		return &createHome{
 			next:              next,
 			logger:            logger,
 			revaGatewayClient: options.RevaGatewayClient,
+			defaultQuota:      defaultQuota,
 		}
 	}
 }
@@ -30,6 +47,7 @@ type createHome struct {
 	next              http.Handler
 	logger            log.Logger
 	revaGatewayClient gateway.GatewayAPIClient
+	defaultQuota      string
 }
 
 func (m createHome) ServeHTTP(w http.ResponseWriter, req *http.Request) {
@@ -45,8 +63,10 @@ func (m createHome) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 	ctx := metadata.AppendToOutgoingContext(req.Context(), revactx.TokenHeader, token)
 
 	createHomeReq := &provider.CreateHomeRequest{}
+	if m.defaultQuota != "" {
+		createHomeReq.Opaque = utils.AppendPlainToOpaque(nil, "quota", m.defaultQuota)
+	}
 	createHomeRes, err := m.revaGatewayClient.CreateHome(ctx, createHomeReq)
-
 	if err != nil {
 		m.logger.Err(err).Msg("error calling CreateHome")
 	} else if createHomeRes.Status.Code != rpc.Code_CODE_OK {
diff --git a/services/proxy/pkg/middleware/options.go b/services/proxy/pkg/middleware/options.go
index 2d07b2b772f..bba1e5132a9 100644
--- a/services/proxy/pkg/middleware/options.go
+++ b/services/proxy/pkg/middleware/options.go
@@ -60,6 +60,8 @@ type Options struct {
 	AccessTokenVerifyMethod string
 	// JWKS sets the options for fetching the JWKS from the IDP
 	JWKS config.JWKS
+	// DefaultSpaceQuota is the default space quota for personal drives
+	DefaultSpaceQuota string
 }
 
 // newOptions initializes the available default options.
@@ -212,3 +214,10 @@ func JWKSOptions(jo config.JWKS) Option {
 		o.JWKS = jo
 	}
 }
+
+// DefaultSpaceQuota sets the default space quota for personal spaces
+func DefaultSpaceQuota(q string) Option {
+	return func(o *Options) {
+		o.DefaultSpaceQuota = q
+	}
+}
diff --git a/services/search/pkg/content/basic.go b/services/search/pkg/content/basic.go
index db9a652fe28..d507255baca 100644
--- a/services/search/pkg/content/basic.go
+++ b/services/search/pkg/content/basic.go
@@ -3,6 +3,7 @@ package content
 import (
 	"context"
 	storageProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
+	"github.com/cs3org/reva/v2/pkg/tags"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
 	"time"
 )
@@ -18,13 +19,19 @@ func NewBasicExtractor(logger log.Logger) (*Basic, error) {
 }
 
 // Extract literally just rearranges the inputs and processes them into a Document.
-func (b Basic) Extract(ctx context.Context, ref *storageProvider.Reference, ri *storageProvider.ResourceInfo) (Document, error) {
+func (b Basic) Extract(_ context.Context, ri *storageProvider.ResourceInfo) (Document, error) {
 	doc := Document{
 		Name:     ri.Path,
 		Size:     ri.Size,
 		MimeType: ri.MimeType,
 	}
 
+	if m := ri.ArbitraryMetadata.GetMetadata(); m != nil {
+		if t, ok := m["tags"]; ok {
+			doc.Tags = tags.FromList(t).AsSlice()
+		}
+	}
+
 	if ri.Mtime != nil {
 		doc.Mtime = time.Unix(int64(ri.Mtime.Seconds), int64(ri.Mtime.Nanos)).UTC().Format(time.RFC3339)
 	}
diff --git a/services/search/pkg/content/content.go b/services/search/pkg/content/content.go
index 9ed2aca6ca4..e58f35dc435 100644
--- a/services/search/pkg/content/content.go
+++ b/services/search/pkg/content/content.go
@@ -9,4 +9,5 @@ type Document struct {
 	Size     uint64
 	Mtime    string
 	MimeType string
+	Tags     []string
 }
diff --git a/services/search/pkg/content/cs3.go b/services/search/pkg/content/cs3.go
index ce3fb96f159..07daa3fa853 100644
--- a/services/search/pkg/content/cs3.go
+++ b/services/search/pkg/content/cs3.go
@@ -5,14 +5,10 @@ import (
 	"crypto/tls"
 	"fmt"
 	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
-	user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
 	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
 	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
-	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
 	revactx "github.com/cs3org/reva/v2/pkg/ctx"
-	"github.com/cs3org/reva/v2/pkg/errtypes"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
-	"google.golang.org/grpc/metadata"
 	"io"
 	"net/http"
 )
@@ -24,53 +20,39 @@ type cs3 struct {
 	secret   string
 }
 
-func newCS3Retriever(client gateway.GatewayAPIClient, logger log.Logger, secret string, insecure bool) cs3 {
+func newCS3Retriever(client gateway.GatewayAPIClient, logger log.Logger, insecure bool) cs3 {
 	return cs3{
 		client:   client,
 		insecure: insecure,
-		secret:   secret,
 		logger:   logger,
 	}
 }
 
 // Retrieve downloads the file from a cs3 service
 // The caller MUST make sure to close the returned ReadCloser
-func (s cs3) Retrieve(ctx context.Context, ref *provider.Reference, owner *user.User) (io.ReadCloser, error) {
-	authRes, err := s.client.Authenticate(
-		ctxpkg.ContextSetUser(ctx, owner),
-		&gateway.AuthenticateRequest{
-			Type:         "machine",
-			ClientId:     "userid:" + owner.GetId().GetOpaqueId(),
-			ClientSecret: s.secret,
-		},
-	)
-	if err == nil && authRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
-		err = errtypes.NewErrtypeFromStatus(authRes.Status)
-	}
-	if err != nil {
-		s.logger.Error().Err(err).Interface("owner", owner).Interface("authRes", authRes).Msg("error using machine auth")
-		return nil, err
+func (s cs3) Retrieve(ctx context.Context, rid *provider.ResourceId) (io.ReadCloser, error) {
+	at, ok := contextGet(ctx, revactx.TokenHeader)
+	if !ok {
+		return nil, fmt.Errorf("context without %s", revactx.TokenHeader)
 	}
 
-	ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, authRes.Token)
-	res, err := s.client.InitiateFileDownload(ctx, &provider.InitiateFileDownloadRequest{Ref: ref})
+	res, err := s.client.InitiateFileDownload(ctx, &provider.InitiateFileDownloadRequest{Ref: &provider.Reference{ResourceId: rid, Path: "."}})
 	if err != nil {
 		return nil, err
 	}
-
 	if res.Status.Code != rpc.Code_CODE_OK {
 		return nil, fmt.Errorf("could not load resoure: %s", res.Status.Message)
 	}
 
-	var ep, tk string
+	var ep, tt string
 	for _, p := range res.Protocols {
 		if p.Protocol == "spaces" {
-			ep, tk = p.DownloadEndpoint, p.Token
+			ep, tt = p.DownloadEndpoint, p.Token
 			break
 		}
 	}
-	if (ep == "" || tk == "") && len(res.Protocols) > 0 {
-		ep, tk = res.Protocols[0].DownloadEndpoint, res.Protocols[0].Token
+	if (ep == "" || tt == "") && len(res.Protocols) > 0 {
+		ep, tt = res.Protocols[0].DownloadEndpoint, res.Protocols[0].Token
 	}
 
 	req, err := http.NewRequest("GET", ep, nil)
@@ -78,8 +60,8 @@ func (s cs3) Retrieve(ctx context.Context, ref *provider.Reference, owner *user.
 		return nil, err
 	}
 
-	req.Header.Set(revactx.TokenHeader, authRes.Token)
-	req.Header.Set("X-Reva-Transfer", tk)
+	req.Header.Set(revactx.TokenHeader, at)
+	req.Header.Set("X-Reva-Transfer", tt)
 
 	client := http.Client{
 		Transport: &http.Transport{
@@ -93,7 +75,7 @@ func (s cs3) Retrieve(ctx context.Context, ref *provider.Reference, owner *user.
 	}
 
 	if cres.StatusCode != http.StatusOK {
-		return nil, fmt.Errorf("could not get the resource \"%s\". Request returned with statuscode %d ", ref.Path, cres.StatusCode)
+		return nil, fmt.Errorf("could not download resource. Request returned with statuscode %d ", cres.StatusCode)
 	}
 
 	return cres.Body, nil
diff --git a/services/search/pkg/content/extractor.go b/services/search/pkg/content/extractor.go
index a32ae8a8a13..29f19bd3487 100644
--- a/services/search/pkg/content/extractor.go
+++ b/services/search/pkg/content/extractor.go
@@ -10,7 +10,7 @@ import (
 //go:generate mockery --name=Extractor
 // Extractor is responsible to extract content and meta information from documents.
 type Extractor interface {
-	Extract(ctx context.Context, ref *provider.Reference, ri *provider.ResourceInfo) (Document, error)
+	Extract(ctx context.Context, ri *provider.ResourceInfo) (Document, error)
 }
 
 func getFirstValue(m map[string][]string, key string) (string, error) {
diff --git a/services/search/pkg/content/mocks/Extractor.go b/services/search/pkg/content/mocks/Extractor.go
index c1b16c8cfe8..1bb1053cad9 100644
--- a/services/search/pkg/content/mocks/Extractor.go
+++ b/services/search/pkg/content/mocks/Extractor.go
@@ -17,20 +17,20 @@ type Extractor struct {
 	mock.Mock
 }
 
-// Extract provides a mock function with given fields: ctx, ref, ri
-func (_m *Extractor) Extract(ctx context.Context, ref *providerv1beta1.Reference, ri *providerv1beta1.ResourceInfo) (content.Document, error) {
-	ret := _m.Called(ctx, ref, ri)
+// Extract provides a mock function with given fields: ctx, ri
+func (_m *Extractor) Extract(ctx context.Context, ri *providerv1beta1.ResourceInfo) (content.Document, error) {
+	ret := _m.Called(ctx, ri)
 
 	var r0 content.Document
-	if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.Reference, *providerv1beta1.ResourceInfo) content.Document); ok {
-		r0 = rf(ctx, ref, ri)
+	if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceInfo) content.Document); ok {
+		r0 = rf(ctx, ri)
 	} else {
 		r0 = ret.Get(0).(content.Document)
 	}
 
 	var r1 error
-	if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.Reference, *providerv1beta1.ResourceInfo) error); ok {
-		r1 = rf(ctx, ref, ri)
+	if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceInfo) error); ok {
+		r1 = rf(ctx, ri)
 	} else {
 		r1 = ret.Error(1)
 	}
diff --git a/services/search/pkg/content/mocks/Retriever.go b/services/search/pkg/content/mocks/Retriever.go
index ce3a4c6abac..0c05f0f3e6f 100644
--- a/services/search/pkg/content/mocks/Retriever.go
+++ b/services/search/pkg/content/mocks/Retriever.go
@@ -9,8 +9,6 @@ import (
 	mock "github.com/stretchr/testify/mock"
 
 	providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
-
-	userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
 )
 
 // Retriever is an autogenerated mock type for the Retriever type
@@ -18,13 +16,13 @@ type Retriever struct {
 	mock.Mock
 }
 
-// Retrieve provides a mock function with given fields: ctx, ref, owner
-func (_m *Retriever) Retrieve(ctx context.Context, ref *providerv1beta1.Reference, owner *userv1beta1.User) (io.ReadCloser, error) {
-	ret := _m.Called(ctx, ref, owner)
+// Retrieve provides a mock function with given fields: ctx, rid
+func (_m *Retriever) Retrieve(ctx context.Context, rid *providerv1beta1.ResourceId) (io.ReadCloser, error) {
+	ret := _m.Called(ctx, rid)
 
 	var r0 io.ReadCloser
-	if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.Reference, *userv1beta1.User) io.ReadCloser); ok {
-		r0 = rf(ctx, ref, owner)
+	if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId) io.ReadCloser); ok {
+		r0 = rf(ctx, rid)
 	} else {
 		if ret.Get(0) != nil {
 			r0 = ret.Get(0).(io.ReadCloser)
@@ -32,8 +30,8 @@ func (_m *Retriever) Retrieve(ctx context.Context, ref *providerv1beta1.Referenc
 	}
 
 	var r1 error
-	if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.Reference, *userv1beta1.User) error); ok {
-		r1 = rf(ctx, ref, owner)
+	if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId) error); ok {
+		r1 = rf(ctx, rid)
 	} else {
 		r1 = ret.Error(1)
 	}
diff --git a/services/search/pkg/content/retriever.go b/services/search/pkg/content/retriever.go
index 1179635bc55..149ccb4d2f1 100644
--- a/services/search/pkg/content/retriever.go
+++ b/services/search/pkg/content/retriever.go
@@ -2,8 +2,8 @@ package content
 
 import (
 	"context"
-	user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
 	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
+	"google.golang.org/grpc/metadata"
 	"io"
 )
 
@@ -11,5 +11,19 @@ import (
 // Retriever is the interface that wraps the basic Retrieve method. 🐕
 // It requests and then returns a resource from the underlying storage.
 type Retriever interface {
-	Retrieve(ctx context.Context, ref *provider.Reference, owner *user.User) (io.ReadCloser, error)
+	Retrieve(ctx context.Context, rid *provider.ResourceId) (io.ReadCloser, error)
+}
+
+func contextGet(ctx context.Context, k string) (string, bool) {
+	md, ok := metadata.FromOutgoingContext(ctx)
+	if !ok {
+		return "", false
+	}
+
+	token, ok := md[k]
+	if len(token) == 0 || !ok {
+		return "", false
+	}
+
+	return token[0], ok
 }
diff --git a/services/search/pkg/content/tika.go b/services/search/pkg/content/tika.go
index 3e4d3b7bd87..53edf9a1495 100644
--- a/services/search/pkg/content/tika.go
+++ b/services/search/pkg/content/tika.go
@@ -6,7 +6,6 @@ import (
 	"fmt"
 	"github.com/bbalet/stopwords"
 	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
-	user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
 	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	"github.com/google/go-tika/tika"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
@@ -39,19 +38,22 @@ func NewTikaExtractor(gw gateway.GatewayAPIClient, logger log.Logger, cfg *confi
 
 	return &Tika{
 		Basic:     basic,
-		Retriever: newCS3Retriever(gw, logger, cfg.MachineAuthAPIKey, cfg.Extractor.CS3AllowInsecure),
+		Retriever: newCS3Retriever(gw, logger, cfg.Extractor.CS3AllowInsecure),
 		tika:      tika.NewClient(nil, cfg.Extractor.Tika.TikaURL),
 	}, nil
 }
 
 // Extract loads a resource from its underlying storage, passes it to tika and processes the result into a Document.
-func (t Tika) Extract(ctx context.Context, ref *provider.Reference, ri *provider.ResourceInfo) (Document, error) {
-	doc, err := t.Basic.Extract(ctx, ref, ri)
+func (t Tika) Extract(ctx context.Context, ri *provider.ResourceInfo) (Document, error) {
+	doc, err := t.Basic.Extract(ctx, ri)
 	if err != nil {
 		return doc, err
 	}
+	if ri.Type != provider.ResourceType_RESOURCE_TYPE_FILE {
+		return doc, nil
+	}
 
-	data, err := t.Retrieve(ctx, ref, &user.User{Id: ri.Owner})
+	data, err := t.Retrieve(ctx, ri.Id)
 	if err != nil {
 		return doc, err
 	}
diff --git a/services/search/pkg/engine/bleve.go b/services/search/pkg/engine/bleve.go
index b2378d2c49d..5fa49f850ce 100644
--- a/services/search/pkg/engine/bleve.go
+++ b/services/search/pkg/engine/bleve.go
@@ -3,12 +3,14 @@ package engine
 import (
 	"context"
 	"errors"
+	"fmt"
 	"github.com/blevesearch/bleve/v2"
 	"github.com/blevesearch/bleve/v2/analysis/analyzer/custom"
 	"github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
 	"github.com/blevesearch/bleve/v2/analysis/token/lowercase"
 	"github.com/blevesearch/bleve/v2/analysis/tokenizer/single"
 	"github.com/blevesearch/bleve/v2/mapping"
+	"github.com/blevesearch/bleve/v2/search/query"
 	storageProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	"github.com/cs3org/reva/v2/pkg/storagespace"
 	"github.com/cs3org/reva/v2/pkg/utils"
@@ -31,7 +33,7 @@ type Bleve struct {
 // NewBleveIndex returns a new bleve index
 // given path must exist.
 func NewBleveIndex(root string) (bleve.Index, error) {
-	destination := filepath.Join(root, "index.bleve")
+	destination := filepath.Join(root, "bleve")
 	index, err := bleve.Open(destination)
 	if err != nil {
 		m, err := BuildBleveMapping()
@@ -56,11 +58,13 @@ func NewBleveEngine(index bleve.Index) *Bleve {
 
 // BuildBleveMapping builds a bleve index mapping which can be used for indexing
 func BuildBleveMapping() (mapping.IndexMapping, error) {
-	nameMapping := bleve.NewTextFieldMapping()
-	nameMapping.Analyzer = "lowercaseKeyword"
+	lowercaseMapping := bleve.NewTextFieldMapping()
+	lowercaseMapping.Analyzer = "lowercaseKeyword"
 
 	docMapping := bleve.NewDocumentMapping()
-	docMapping.AddFieldMappingsAt("Name", nameMapping)
+	docMapping.AddFieldMappingsAt("Name", lowercaseMapping)
+	docMapping.AddFieldMappingsAt("Tags", lowercaseMapping)
+	docMapping.AddFieldMappingsAt("Content", lowercaseMapping)
 
 	indexMapping := bleve.NewIndexMapping()
 	indexMapping.DefaultAnalyzer = keyword.Name
@@ -82,22 +86,35 @@ func BuildBleveMapping() (mapping.IndexMapping, error) {
 
 // Search executes a search request operation within the index.
 // Returns a SearchIndexResponse object or an error.
-func (b *Bleve) Search(ctx context.Context, sir *searchService.SearchIndexRequest) (*searchService.SearchIndexResponse, error) {
-	deletedQuery := bleve.NewBoolFieldQuery(false)
-	deletedQuery.SetField("Deleted")
-	query := bleve.NewConjunctionQuery(
-		bleve.NewQueryStringQuery(b.buildQuery(sir.Query)),
-		deletedQuery, // Skip documents that have been marked as deleted
-		bleve.NewQueryStringQuery("RootID:"+storagespace.FormatResourceID(
-			storageProvider.ResourceId{
-				StorageId: sir.Ref.GetResourceId().GetStorageId(),
-				SpaceId:   sir.Ref.GetResourceId().GetSpaceId(),
-				OpaqueId:  sir.Ref.GetResourceId().GetOpaqueId(),
-			})), // Limit search to the space
-		bleve.NewQueryStringQuery("Path:"+escapeQuery(utils.MakeRelativePath(path.Join(sir.Ref.Path, "/"))+"*")), // Limit search to this directory in the space
+func (b *Bleve) Search(_ context.Context, sir *searchService.SearchIndexRequest) (*searchService.SearchIndexResponse, error) {
+	q := bleve.NewConjunctionQuery(
+		&query.QueryStringQuery{
+			Query: b.buildQuery(sir.Query),
+		},
+		// Skip documents that have been marked as deleted
+		&query.BoolFieldQuery{
+			Bool:     false,
+			FieldVal: "Deleted",
+		},
+		&query.QueryStringQuery{
+			Query: fmt.Sprintf(
+				"RootID:%s",
+				storagespace.FormatResourceID(
+					storageProvider.ResourceId{
+						StorageId: sir.Ref.GetResourceId().GetStorageId(),
+						SpaceId:   sir.Ref.GetResourceId().GetSpaceId(),
+						OpaqueId:  sir.Ref.GetResourceId().GetOpaqueId(),
+					},
+				),
+			),
+		},
+		// Limit search to this directory in the space
+		&query.QueryStringQuery{
+			Query: fmt.Sprintf("Path:%s*", escapeQuery(utils.MakeRelativePath(path.Join(sir.Ref.Path, "/")))),
+		},
 	)
 
-	bleveReq := bleve.NewSearchRequest(query)
+	bleveReq := bleve.NewSearchRequest(q)
 	bleveReq.Size = 200
 	if sir.PageSize > 0 {
 		bleveReq.Size = int(sir.PageSize)
@@ -133,6 +150,7 @@ func (b *Bleve) Search(ctx context.Context, sir *searchService.SearchIndexReques
 				Type:     uint64(getValue[float64](hit.Fields, "Type")),
 				MimeType: getValue[string](hit.Fields, "MimeType"),
 				Deleted:  getValue[bool](hit.Fields, "Deleted"),
+				Tags:     getValue[[]string](hit.Fields, "Tags"),
 			},
 		}
 
@@ -172,11 +190,11 @@ func (b *Bleve) Move(id string, target string) error {
 	}
 
 	if r.Type == uint64(storageProvider.ResourceType_RESOURCE_TYPE_CONTAINER) {
-		query := bleve.NewConjunctionQuery(
+		q := bleve.NewConjunctionQuery(
 			bleve.NewQueryStringQuery("RootID:"+r.RootID),
 			bleve.NewQueryStringQuery("Path:"+escapeQuery(oldName+"/*")),
 		)
-		bleveReq := bleve.NewSearchRequest(query)
+		bleveReq := bleve.NewSearchRequest(q)
 		bleveReq.Size = math.MaxInt
 		bleveReq.Fields = []string{"*"}
 		res, err := b.index.Search(bleveReq)
@@ -245,6 +263,7 @@ func (b *Bleve) getResource(id string) (*Resource, error) {
 			Size:     uint64(getValue[float64](fields, "Size")),
 			Mtime:    getValue[string](fields, "Mtime"),
 			MimeType: getValue[string](fields, "MimeType"),
+			Tags:     getValue[[]string](fields, "Tags"),
 		},
 	}, nil
 }
@@ -269,11 +288,11 @@ func (b *Bleve) setDeleted(id string, deleted bool) error {
 	}
 
 	if it.Type == uint64(storageProvider.ResourceType_RESOURCE_TYPE_CONTAINER) {
-		query := bleve.NewConjunctionQuery(
+		q := bleve.NewConjunctionQuery(
 			bleve.NewQueryStringQuery("RootID:"+it.RootID),
 			bleve.NewQueryStringQuery("Path:"+escapeQuery(it.Path+"/*")),
 		)
-		bleveReq := bleve.NewSearchRequest(query)
+		bleveReq := bleve.NewSearchRequest(q)
 		bleveReq.Size = math.MaxInt
 		bleveReq.Fields = []string{"*"}
 		res, err := b.index.Search(bleveReq)
@@ -294,17 +313,16 @@ func (b *Bleve) setDeleted(id string, deleted bool) error {
 	return nil
 }
 
-func (b Bleve) buildQuery(q string) string {
-	query := q
-	fields := []string{"RootID", "Path", "ID", "Name", "Size", "Mtime", "MimeType", "Type", "Content", "Title"}
+func (b *Bleve) buildQuery(qs string) string {
+	fields := []string{"RootID", "Path", "ID", "Name", "Size", "Mtime", "MimeType", "Type", "Content", "Title", "Tags"}
 	for _, field := range fields {
-		query = strings.ReplaceAll(query, strings.ToLower(field)+":", field+":")
+		qs = strings.ReplaceAll(qs, strings.ToLower(field)+":", field+":")
 	}
 
-	if strings.Contains(query, ":") {
-		return query // Sophisticated field based search
+	if strings.Contains(qs, ":") {
+		return qs // Sophisticated field based search
 	}
 
 	// this is a basic filename search
-	return "Name:*" + strings.ReplaceAll(strings.ToLower(query), " ", `\ `) + "*"
+	return "Name:*" + strings.ReplaceAll(strings.ToLower(qs), " ", `\ `) + "*"
 }
diff --git a/services/search/pkg/search/events.go b/services/search/pkg/search/events.go
index 07542070337..69246a89734 100644
--- a/services/search/pkg/search/events.go
+++ b/services/search/pkg/search/events.go
@@ -4,11 +4,10 @@ import (
 	"context"
 	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
 	user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
-	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
-	rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
 	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	"github.com/cs3org/reva/v2/pkg/events"
 	"github.com/cs3org/reva/v2/pkg/storagespace"
+	"github.com/cs3org/reva/v2/pkg/utils"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
 	"github.com/owncloud/ocis/v2/services/search/pkg/config"
 	"github.com/owncloud/ocis/v2/services/search/pkg/content"
@@ -26,48 +25,53 @@ type eventHandler struct {
 // HandleEvents listens to the needed events,
 // it handles the whole resource indexing livecycle.
 func HandleEvents(eng engine.Engine, extractor content.Extractor, gw gateway.GatewayAPIClient, bus events.Stream, logger log.Logger, cfg *config.Config) error {
-	ch, err := events.Consume(
-		bus,
-		"search",
+	evts := []events.Unmarshaller{
 		events.ItemTrashed{},
 		events.ItemRestored{},
 		events.ItemMoved{},
 		events.ContainerCreated{},
 		events.FileTouched{},
 		events.FileVersionRestored{},
-		events.FileUploaded{},
-		events.UploadReady{},
-	)
+		events.TagsAdded{},
+		events.TagsRemoved{},
+	}
+
+	if cfg.Events.AsyncUploads {
+		evts = append(evts, events.UploadReady{})
+	} else {
+		evts = append(evts, events.FileUploaded{})
+	}
+
+	ch, err := events.Consume(bus, "search", evts...)
 	if err != nil {
 		return err
 	}
 
 	go func(eh *eventHandler, ch <-chan interface{}) {
 		for e := range ch {
-
 			eh.logger.Debug().Interface("event", e).Msg("updating index")
 
 			switch ev := e.(type) {
 			case events.ItemTrashed:
 				eh.trashItem(ev.ID)
 			case events.ItemMoved:
-				eh.moveItem(ev.Ref, &user.User{Id: ev.Executant})
+				eh.moveItem(ev.Ref, ev.Executant)
 			case events.ItemRestored:
-				eh.restoreItem(ev.Ref, &user.User{Id: ev.Executant})
+				eh.restoreItem(ev.Ref, ev.Executant)
 			case events.ContainerCreated:
-				eh.upsertItem(ev.Ref, &user.User{Id: ev.Executant})
+				eh.upsertItem(ev.Ref, ev.Executant)
 			case events.FileTouched:
-				eh.upsertItem(ev.Ref, &user.User{Id: ev.Executant})
+				eh.upsertItem(ev.Ref, ev.Executant)
 			case events.FileVersionRestored:
-				eh.upsertItem(ev.Ref, &user.User{Id: ev.Executant})
+				eh.upsertItem(ev.Ref, ev.Executant)
 			case events.FileUploaded:
-				if cfg.Events.AsyncUploads {
-					return
-				}
-
-				eh.upsertItem(ev.Ref, &user.User{Id: ev.Executant})
+				eh.upsertItem(ev.Ref, ev.Executant)
 			case events.UploadReady:
-				eh.upsertItem(ev.FileRef, ev.ExecutingUser)
+				eh.upsertItem(ev.FileRef, ev.ExecutingUser.Id)
+			case events.TagsAdded:
+				eh.upsertItem(ev.Ref, ev.Executant)
+			case events.TagsRemoved:
+				eh.upsertItem(ev.Ref, ev.Executant)
 			}
 		}
 	}(
@@ -91,92 +95,74 @@ func (eh *eventHandler) trashItem(rid *provider.ResourceId) {
 	}
 }
 
-func (eh *eventHandler) upsertItem(ref *provider.Reference, owner *user.User) {
-	if ref == nil || owner == nil {
+func (eh *eventHandler) upsertItem(ref *provider.Reference, uid *user.UserId) {
+	ctx, stat, path := eh.resInfo(uid, ref)
+	if ctx == nil || stat == nil || path == nil {
 		return
 	}
 
-	statRes, err := statResource(ref, owner, eh.gateway, eh.secret, eh.logger)
-	if err != nil {
-		eh.logger.Error().Err(err).Msg("failed to stat the changed resource")
-		return
-	}
-	if statRes.Status.Code != rpc.Code_CODE_OK {
-		eh.logger.Error().Interface("statRes", statRes).Msg("failed to stat the changed resource")
-		return
-	}
-
-	doc, err := eh.extractor.Extract(context.TODO(), ref, statRes.Info)
+	doc, err := eh.extractor.Extract(ctx, stat.Info)
 	if err != nil {
 		eh.logger.Error().Err(err).Msg("failed to extract resource content")
 		return
 	}
 
 	r := engine.Resource{
-		ID:       storagespace.FormatResourceID(*statRes.Info.Id),
-		RootID:   storagespace.FormatResourceID(*ref.ResourceId),
-		Path:     ref.Path,
-		Type:     uint64(statRes.Info.Type),
+		ID: storagespace.FormatResourceID(*stat.Info.Id),
+		RootID: storagespace.FormatResourceID(provider.ResourceId{
+			StorageId: stat.Info.Id.StorageId,
+			OpaqueId:  stat.Info.Id.SpaceId,
+			SpaceId:   stat.Info.Id.SpaceId,
+		}),
+		Path:     utils.MakeRelativePath(path.Path),
+		Type:     uint64(stat.Info.Type),
 		Document: doc,
 	}
 
-	err = eh.engine.Upsert(r.ID, r)
-	if err != nil {
+	if err = eh.engine.Upsert(r.ID, r); err != nil {
 		eh.logger.Error().Err(err).Msg("error adding updating the resource in the index")
 	} else {
 		logDocCount(eh.engine, eh.logger)
 	}
 }
 
-func (eh *eventHandler) restoreItem(ref *provider.Reference, owner *user.User) {
-	if ref == nil || owner == nil {
+func (eh *eventHandler) restoreItem(ref *provider.Reference, uid *user.UserId) {
+	ctx, stat, path := eh.resInfo(uid, ref)
+	if ctx == nil || stat == nil || path == nil {
 		return
 	}
 
-	statRes, err := statResource(ref, owner, eh.gateway, eh.secret, eh.logger)
-	if err != nil {
-		eh.logger.Error().Err(err).Msg("failed to stat the changed resource")
-		return
+	if err := eh.engine.Restore(storagespace.FormatResourceID(*stat.Info.Id)); err != nil {
+		eh.logger.Error().Err(err).Msg("failed to restore the changed resource in the index")
 	}
+}
 
-	if statRes.Status.Code != rpc.Code_CODE_OK {
-		eh.logger.Error().Interface("statRes", statRes).Msg("failed to stat the changed resource")
+func (eh *eventHandler) moveItem(ref *provider.Reference, uid *user.UserId) {
+	ctx, stat, path := eh.resInfo(uid, ref)
+	if ctx == nil || stat == nil || path == nil {
 		return
 	}
 
-	err = eh.engine.Restore(storagespace.FormatResourceID(*statRes.Info.Id))
-	if err != nil {
-		eh.logger.Error().Err(err).Msg("failed to restore the changed resource in the index")
+	if err := eh.engine.Move(storagespace.FormatResourceID(*stat.Info.Id), path.Path); err != nil {
+		eh.logger.Error().Err(err).Msg("failed to move the changed resource in the index")
 	}
 }
 
-func (eh *eventHandler) moveItem(ref *provider.Reference, owner *user.User) {
-	if ref == nil || owner == nil {
-		return
-	}
-
-	statRes, err := statResource(ref, owner, eh.gateway, eh.secret, eh.logger)
+func (eh *eventHandler) resInfo(uid *user.UserId, ref *provider.Reference) (context.Context, *provider.StatResponse, *provider.GetPathResponse) {
+	ownerCtx, err := getAuthContext(&user.User{Id: uid}, eh.gateway, eh.secret, eh.logger)
 	if err != nil {
-		eh.logger.Error().Err(err).Msg("failed to stat the moved resource")
-		return
-	}
-	if statRes.Status.Code != rpc.Code_CODE_OK {
-		eh.logger.Error().Interface("statRes", statRes).Msg("failed to stat the moved resource")
-		return
+		return nil, nil, nil
 	}
 
-	gpRes, err := getPath(statRes.Info.Id, owner, eh.gateway, eh.secret, eh.logger)
+	statRes, err := statResource(ownerCtx, ref, eh.gateway, eh.logger)
 	if err != nil {
-		eh.logger.Error().Err(err).Interface("ref", ref).Msg("failed to get path for moved resource")
-		return
-	}
-	if gpRes.Status.Code != rpcv1beta1.Code_CODE_OK {
-		eh.logger.Error().Interface("status", gpRes.Status).Interface("ref", ref).Msg("failed to get path for moved resource")
-		return
+		return nil, nil, nil
 	}
 
-	err = eh.engine.Move(storagespace.FormatResourceID(*statRes.Info.Id), gpRes.Path)
+	pathRes, err := getPath(ownerCtx, statRes.Info.Id, eh.gateway, eh.logger)
 	if err != nil {
-		eh.logger.Error().Err(err).Msg("failed to move the changed resource in the index")
+		return nil, nil, nil
 	}
+
+	return ownerCtx, statRes, pathRes
 }
diff --git a/services/search/pkg/search/provider.go b/services/search/pkg/search/provider.go
index 4c35fe7ad8a..0ea5f583dcd 100644
--- a/services/search/pkg/search/provider.go
+++ b/services/search/pkg/search/provider.go
@@ -213,7 +213,7 @@ func (p *Provider) IndexSpace(ctx context.Context, req *searchsvc.IndexSpaceRequ
 			ResourceId: &rootId,
 		}
 
-		doc, err := p.extractor.Extract(ctx, ref, info)
+		doc, err := p.extractor.Extract(ctx, info)
 		if err != nil {
 			p.logger.Error().Err(err).Msg("error extracting content")
 		}
diff --git a/services/search/pkg/search/search.go b/services/search/pkg/search/search.go
index a204e8ac722..40b8474cd9f 100644
--- a/services/search/pkg/search/search.go
+++ b/services/search/pkg/search/search.go
@@ -2,6 +2,7 @@ package search
 
 import (
 	"context"
+	"errors"
 	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
 	user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
 	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
@@ -54,20 +55,34 @@ func getAuthContext(owner *user.User, gw gateway.GatewayAPIClient, secret string
 	return metadata.AppendToOutgoingContext(ownerCtx, ctxpkg.TokenHeader, authRes.Token), nil
 }
 
-func statResource(ref *provider.Reference, owner *user.User, gw gateway.GatewayAPIClient, secret string, logger log.Logger) (*provider.StatResponse, error) {
-	ownerCtx, err := getAuthContext(owner, gw, secret, logger)
+func statResource(ctx context.Context, ref *provider.Reference, gw gateway.GatewayAPIClient, logger log.Logger) (*provider.StatResponse, error) {
+	res, err := gw.Stat(ctx, &provider.StatRequest{Ref: ref})
 	if err != nil {
+		logger.Error().Err(err).Msg("failed to stat the moved resource")
+		return nil, err
+	}
+	if res.Status.Code != rpc.Code_CODE_OK {
+		err := errors.New("failed to stat the moved resource")
+		logger.Error().Interface("res", res).Msg(err.Error())
 		return nil, err
 	}
 
-	return gw.Stat(ownerCtx, &provider.StatRequest{Ref: ref})
+	return res, nil
 }
 
-func getPath(id *provider.ResourceId, owner *user.User, gw gateway.GatewayAPIClient, secret string, logger log.Logger) (*provider.GetPathResponse, error) {
-	ownerCtx, err := getAuthContext(owner, gw, secret, logger)
+func getPath(ctx context.Context, id *provider.ResourceId, gw gateway.GatewayAPIClient, logger log.Logger) (*provider.GetPathResponse, error) {
+	res, err := gw.GetPath(ctx, &provider.GetPathRequest{ResourceId: id})
+
 	if err != nil {
+		logger.Error().Err(err).Interface("id", id).Msg("failed to get path for moved resource")
+		return nil, err
+	}
+	if res.Status.Code != rpc.Code_CODE_OK {
+		err := errors.New("failed to get path for moved resource")
+
+		logger.Error().Interface("status", res.Status).Interface("id", id).Msg(err.Error())
 		return nil, err
 	}
 
-	return gw.GetPath(ownerCtx, &provider.GetPathRequest{ResourceId: id})
+	return res, nil
 }


Test patch:
diff --git a/ocis-pkg/bytesize/bytesize_test.go b/ocis-pkg/bytesize/bytesize_test.go
new file mode 100644
index 00000000000..d17819a13cf
--- /dev/null
+++ b/ocis-pkg/bytesize/bytesize_test.go
@@ -0,0 +1,106 @@
+package bytesize_test
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/owncloud/ocis/v2/ocis-pkg/bytesize"
+	"github.com/test-go/testify/require"
+)
+
+func TestParseSpecial(t *testing.T) {
+	testCases := []struct {
+		Alias          string
+		Input          string
+		ExpectedOutput uint64
+		ExpectError    bool
+	}{
+		{
+			Alias:          "it assumes bytes",
+			Input:          "100",
+			ExpectedOutput: 100,
+		},
+		{
+			Alias:          "it accepts a space between value and unit",
+			Input:          "1 MB",
+			ExpectedOutput: 1000000,
+		},
+		{
+			Alias:          "it accepts also more spaces between value and unit",
+			Input:          "1                                            MB",
+			ExpectedOutput: 1000000,
+		},
+		{
+			Alias:          "it ignores trailing and leading spaces",
+			Input:          " 1MB ",
+			ExpectedOutput: 1000000,
+		},
+		{
+			Alias:          "it errors on unknown units",
+			Input:          "1SB",
+			ExpectedOutput: 0,
+			ExpectError:    true,
+		},
+		{
+			Alias:          "it multiplies correctly",
+			Input:          "16MB",
+			ExpectedOutput: 16000000,
+		},
+		{
+			Alias:          "it errors when no value is given",
+			Input:          "GB",
+			ExpectedOutput: 0,
+			ExpectError:    true,
+		},
+		{
+			Alias:          "it errors when bad input is given",
+			Input:          ",as!@@delta",
+			ExpectedOutput: 0,
+			ExpectError:    true,
+		},
+		{
+			Alias:          "it errors when using floats",
+			Input:          "1.024GB",
+			ExpectedOutput: 0,
+			ExpectError:    true,
+		},
+	}
+
+	for _, tc := range testCases {
+		actual, err := bytesize.Parse(tc.Input)
+		if tc.ExpectError {
+			require.Error(t, err)
+		} else {
+			require.NoError(t, err)
+		}
+
+		require.Equal(t, tc.ExpectError, err != nil, tc.Alias)
+		require.Equal(t, int(tc.ExpectedOutput), int(actual), tc.Alias)
+	}
+}
+
+func TestParseHappy(t *testing.T) {
+	testCases := []struct {
+		Input    string
+		Expected uint64
+	}{
+		{Input: "1", Expected: 1},
+		{Input: "1KB", Expected: 1000},
+		{Input: "1MB", Expected: 1000000},
+		{Input: "1GB", Expected: 1000000000},
+		{Input: "1TB", Expected: 1000000000000},
+		{Input: "1PB", Expected: 1000000000000000},
+		{Input: "1EB", Expected: 1000000000000000000},
+		{Input: "1MiB", Expected: 1048576},
+		{Input: "1GiB", Expected: 1073741824},
+		{Input: "1TiB", Expected: 1099511627776},
+		{Input: "1PiB", Expected: 1125899906842624},
+		{Input: "1EiB", Expected: 1152921504606846976},
+	}
+
+	for _, tc := range testCases {
+		actual, err := bytesize.Parse(tc.Input)
+		require.NoError(t, err)
+		require.Equal(t, int(tc.Expected), int(actual), fmt.Sprintf("case %s", tc.Input))
+	}
+}
diff --git a/services/search/pkg/content/basic_test.go b/services/search/pkg/content/basic_test.go
index ebd2342eeb3..5c08f53218f 100644
--- a/services/search/pkg/content/basic_test.go
+++ b/services/search/pkg/content/basic_test.go
@@ -14,13 +14,11 @@ var _ = Describe("Basic", func() {
 	var (
 		basic  content.Extractor
 		logger = log.NewLogger()
-		ref    = &storageProvider.Reference{}
 		ctx    = context.TODO()
 	)
 
 	BeforeEach(func() {
 		basic, _ = content.NewBasicExtractor(logger)
-		ref = &storageProvider.Reference{}
 	})
 
 	Describe("extract", func() {
@@ -31,7 +29,7 @@ var _ = Describe("Basic", func() {
 				MimeType: "application/pdf",
 			}
 
-			doc, err := basic.Extract(ctx, ref, ri)
+			doc, err := basic.Extract(ctx, ri)
 
 			Expect(err).To(BeNil())
 			Expect(doc).ToNot(BeNil())
@@ -40,6 +38,31 @@ var _ = Describe("Basic", func() {
 			Expect(doc.MimeType).To(Equal(ri.MimeType))
 		})
 
+		It("adds tags", func() {
+			for _, data := range []struct {
+				tags   string
+				expect []string
+			}{
+				{tags: "", expect: []string{}},
+				{tags: ",,,", expect: []string{}},
+				{tags: ",foo,,", expect: []string{"foo"}},
+				{tags: ",foo,,bar,", expect: []string{"foo", "bar"}},
+			} {
+				ri := &storageProvider.ResourceInfo{
+					ArbitraryMetadata: &storageProvider.ArbitraryMetadata{
+						Metadata: map[string]string{
+							"tags": data.tags,
+						},
+					},
+				}
+
+				doc, err := basic.Extract(ctx, ri)
+				Expect(err).To(BeNil())
+				Expect(doc).ToNot(BeNil())
+				Expect(doc.Tags).To(Equal(data.expect))
+			}
+		})
+
 		It("RFC3339 mtime", func() {
 			for _, data := range []struct {
 				second uint64
@@ -55,7 +78,7 @@ var _ = Describe("Basic", func() {
 					ri.Mtime = &cs3Types.Timestamp{Seconds: data.second}
 				}
 
-				doc, err := basic.Extract(ctx, ref, ri)
+				doc, err := basic.Extract(ctx, ri)
 				Expect(err).To(BeNil())
 				Expect(doc).ToNot(BeNil())
 				Expect(doc.Mtime).To(Equal(data.expect))
diff --git a/services/search/pkg/content/tika_test.go b/services/search/pkg/content/tika_test.go
index 61d8044d295..d15493e7086 100644
--- a/services/search/pkg/content/tika_test.go
+++ b/services/search/pkg/content/tika_test.go
@@ -21,7 +21,6 @@ var _ = Describe("Tika", func() {
 	Describe("extract", func() {
 		var (
 			body     string
-			title    string
 			language string
 			version  string
 			srv      *httptest.Server
@@ -30,7 +29,6 @@ var _ = Describe("Tika", func() {
 
 		BeforeEach(func() {
 			body = ""
-			title = ""
 			language = ""
 			version = ""
 			srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -41,7 +39,7 @@ var _ = Describe("Tika", func() {
 				case "/language/stream":
 					out = language
 				case "/rmeta/text":
-					out = fmt.Sprintf(`[{"X-TIKA:content":"%s"},{"title":"%s"}]`, body, title)
+					out = fmt.Sprintf(`[{"X-TIKA:content":"%s"}]`, body)
 				}
 
 				_, _ = w.Write([]byte(out))
@@ -65,21 +63,29 @@ var _ = Describe("Tika", func() {
 			srv.Close()
 		})
 
-		It("basic operations", func() {
+		It("skips non file resources", func() {
+			doc, err := tika.Extract(context.TODO(), &provider.ResourceInfo{})
+			Expect(err).ToNot(HaveOccurred())
+			Expect(doc.Content).To(Equal(""))
+		})
+
+		It("adds content", func() {
 			body = "any body"
-			title = "any title"
 
-			doc, err := tika.Extract(context.TODO(), &provider.Reference{}, &provider.ResourceInfo{})
+			doc, err := tika.Extract(context.TODO(), &provider.ResourceInfo{
+				Type: provider.ResourceType_RESOURCE_TYPE_FILE,
+			})
 			Expect(err).ToNot(HaveOccurred())
-			Expect(doc.Title).To(Equal(title))
 			Expect(doc.Content).To(Equal(body))
 		})
 
-		It("remove stop words", func() {
+		It("removes stop words", func() {
 			body = "body to test stop words!!! I, you, he, she, it, we, you, they, stay"
 			language = "en"
 
-			doc, err := tika.Extract(context.TODO(), &provider.Reference{}, &provider.ResourceInfo{})
+			doc, err := tika.Extract(context.TODO(), &provider.ResourceInfo{
+				Type: provider.ResourceType_RESOURCE_TYPE_FILE,
+			})
 			Expect(err).ToNot(HaveOccurred())
 			Expect(doc.Content).To(Equal("body test stop words i stay"))
 		})
diff --git a/services/search/pkg/engine/bleve_test.go b/services/search/pkg/engine/bleve_test.go
index 15358925f61..4d0311d29c7 100644
--- a/services/search/pkg/engine/bleve_test.go
+++ b/services/search/pkg/engine/bleve_test.go
@@ -94,6 +94,25 @@ var _ = Describe("Bleve", func() {
 				assertDocCount(rid, `Size:<1000`, 0)
 				assertDocCount(rid, `Size:>100000`, 0)
 			})
+
+			It("finds files by tags", func() {
+				rid := sprovider.ResourceId{
+					StorageId: "1",
+					SpaceId:   "2",
+					OpaqueId:  "3",
+				}
+				r := createEntity(rid, content.Document{Tags: []string{"foo", "bar"}})
+				err := eng.Upsert(r.ID, r)
+				Expect(err).ToNot(HaveOccurred())
+
+				assertDocCount(rid, `Tags:foo`, 1)
+				assertDocCount(rid, `Tags:bar`, 1)
+				assertDocCount(rid, `Tags:foo Tags:bar`, 1)
+				assertDocCount(rid, `Tags:foo Tags:bar Tags:baz`, 1)
+				assertDocCount(rid, `+Tags:foo +Tags:bar Tags:baz`, 1)
+				assertDocCount(rid, `+Tags:foo +Tags:bar +Tags:baz`, 0)
+				assertDocCount(rid, `Tags:baz`, 0)
+			})
 		})
 
 		Context("by filename", func() {
@@ -289,9 +308,5 @@ var _ = Describe("Bleve", func() {
 				})
 			})
 		})
-
-		Context("query", func() {
-
-		})
 	})
 })
diff --git a/services/search/pkg/search/events_test.go b/services/search/pkg/search/events_test.go
index cc7c0f98768..a0ebecc0e13 100644
--- a/services/search/pkg/search/events_test.go
+++ b/services/search/pkg/search/events_test.go
@@ -66,15 +66,19 @@ var _ = Describe("Events", func() {
 			Token:  "authtoken",
 		}, nil)
 		gw.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{
-			Status: status.NewOK(context.Background()),
+			Status: status.NewOK(ctx),
 			Info:   ri,
 		}, nil)
+		gw.On("GetPath", mock.Anything, mock.Anything).Return(&provider.GetPathResponse{
+			Status: status.NewOK(ctx),
+			Path:   "",
+		}, nil)
 		engine.On("DocCount").Return(uint64(1), nil)
 	})
 	Describe("events", func() {
 		It("triggers an index update when a file has been uploaded", func() {
 			called := false
-			extractor.Mock.On("Extract", mock.Anything, mock.Anything, mock.Anything).Return(content.Document{}, nil)
+			extractor.Mock.On("Extract", mock.Anything, mock.Anything).Return(content.Document{}, nil)
 			engine.On("Upsert", mock.Anything, mock.Anything).Return(nil).Run(func(args mock.Arguments) {
 				called = true
 			})
@@ -160,10 +164,6 @@ var _ = Describe("Events", func() {
 
 		It("indexes items when they are being moved", func() {
 			called := false
-			gw.On("GetPath", mock.Anything, mock.Anything).Return(&provider.GetPathResponse{
-				Status: status.NewOK(ctx),
-				Path:   "",
-			}, nil)
 			engine.On("Move", mock.Anything, mock.Anything).Return(nil).Run(func(args mock.Arguments) {
 				called = true
 			})