Repository: kopia/kopia
Pull request #2615 (2022-12-01 23:01:00)

build(deps-dev): bump playwright from 1.26.1 to 1.28.1 in /app
Bumps [playwright](https://github.com/Microsoft/playwright) from 1.26.1 to 1.28.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/Microsoft/playwright/releases">playwright's releases</a>.</em></p>
<blockquote>
<h2>v1.28.1</h2>
<h2>Highlights</h2>
<p>This patch release includes the following bug fixes:</p>
<p><a href="https://github-redirect.dependabot.com/microsoft/playwright/issues/18928">microsoft/playwright#18928</a> - [BUG] Electron firstWindow times out after upgrading to 1.28.0
<a href="https://github-redirect.dependabot.com/microsoft/playwright/issues/18920">microsoft/playwright#18920</a> - [BUG] [expanded=false] in role selector returns elements without aria-expanded attribute
<a href="https://github-redirect.dependabot.com/microsoft/playwright/issues/18865">microsoft/playwright#18865</a> - [BUG] regression in killing web server process in 1.28.0</p>
<h2>Browser Versions</h2>
<ul>
<li>Chromium 108.0.5359.29</li>
<li>Mozilla Firefox 106.0</li>
<li>WebKit 16.4</li>
</ul>
<p>This version was also tested against the following stable channels:</p>
<ul>
<li>Google Chrome 107</li>
<li>Microsoft Edge 107</li>
</ul>
<h2>v1.28</h2>
<h2>Playwright Tools</h2>
<ul>
<li><strong>Record at Cursor in VSCode.</strong> You can run the test, position the cursor at the end of the test and continue generating the test.</li>
</ul>
<!-- raw HTML omitted -->
<ul>
<li><strong>Live Locators in VSCode.</strong> You can hover and edit locators in VSCode to get them  highlighted in the opened browser.</li>
<li><strong>Live Locators in CodeGen.</strong> Generate a locator for any element on the page using &quot;Explore&quot; tool.</li>
</ul>
<!-- raw HTML omitted -->
<ul>
<li><strong>Codegen and Trace Viewer Dark Theme.</strong> Automatically picked up from operating system settings.</li>
</ul>
<!-- raw HTML omitted -->
<h3>Test Runner</h3>
<ul>
<li>
<p>Configure retries and test timeout for a file or a test with <a href="https://playwright.dev/docs/api/class-test#test-describe-configure"><code>test.describe.configure([options])</code></a>.</p>
<pre lang="js"><code>// Each test in the file will be retried twice and have a timeout of 20 seconds.
test.describe.configure({ retries: 2, timeout: 20_000 });
test('runs first', async ({ page }) =&gt; {});
test('runs second', async ({ page }) =&gt; {});
</code></pre>
</li>
<li>
<p>Use <a href="https://playwright.dev/docs/api/class-testproject#test-project-snapshot-path-template"><code>testProject.snapshotPathTemplate</code></a> and <a href="https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-path-template"><code>testConfig.snapshotPathTemplate</code></a> to configure a template controlling location of snapshots generated by <a href="https://playwright.dev/docs/test-assertions#page-assertions-to-have-screenshot-1"><code>expect(page).toHaveScreenshot(name[, options])</code></a> and <a href="https://playwright.dev/docs/test-assertions#screenshot-assertions-to-match-snapshot-1"><code>expect(screenshot).toMatchSnapshot(name[, options])</code></a>.</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/microsoft/playwright/commit/9ede66abd60797cd516484fb0bf172b76b7aec5b"><code>9ede66a</code></a> cherry-pick(<a href="https://github-redirect.dependabot.com/Microsoft/playwright/issues/18997">#18997</a>): fix(reuse): stop pending operations upon reuse/disconnect</li>
<li><a href="https://github.com/microsoft/playwright/commit/76dc43e9bacc1cad70c957f007e195bf6e210e0a"><code>76dc43e</code></a> cherry-pick(<a href="https://github-redirect.dependabot.com/Microsoft/playwright/issues/18993">#18993</a>): fix(electron): tolerate connection closed during quit</li>
<li><a href="https://github.com/microsoft/playwright/commit/f38b2c86eb6675453654b5adb7a42d5bc01a5ae2"><code>f38b2c8</code></a> chore: mark v1.28.1 (<a href="https://github-redirect.dependabot.com/Microsoft/playwright/issues/18974">#18974</a>)</li>
<li><a href="https://github.com/microsoft/playwright/commit/0d5098b7c051d71adcd1a7e516d9e98d5a869458"><code>0d5098b</code></a> cherry-pick(<a href="https://github-redirect.dependabot.com/Microsoft/playwright/issues/18968">#18968</a>): Revert &quot;fix(webserver): do not spawn webserver as a new ...</li>
<li><a href="https://github.com/microsoft/playwright/commit/bbe27dc564941aa228a2d001cb4fe1f9dfd60875"><code>bbe27dc</code></a> cherry-pick(<a href="https://github-redirect.dependabot.com/Microsoft/playwright/issues/18929">#18929</a>): fix(role selector): expanded=false does not match elemen...</li>
<li><a href="https://github.com/microsoft/playwright/commit/226eedf01926339e49768748981264296b4bf70b"><code>226eedf</code></a> cherry-pick(<a href="https://github-redirect.dependabot.com/Microsoft/playwright/issues/18972">#18972</a>): fix(electron): consistently emit ready event after app i...</li>
<li><a href="https://github.com/microsoft/playwright/commit/36f8a6399c8dfe894526f6022d492d1226a33801"><code>36f8a63</code></a> chery-pick(<a href="https://github-redirect.dependabot.com/Microsoft/playwright/issues/18884">#18884</a>): docs(locator): code snippet fixes (<a href="https://github-redirect.dependabot.com/Microsoft/playwright/issues/18887">#18887</a>)</li>
<li><a href="https://github.com/microsoft/playwright/commit/d284219c32e2dacf1e20665cade3fbfd7c1fa67f"><code>d284219</code></a> cherry-pick(<a href="https://github-redirect.dependabot.com/Microsoft/playwright/issues/18854">#18854</a>): docs: Java, .NET, Python release notes 1.28 (<a href="https://github-redirect.dependabot.com/Microsoft/playwright/issues/18858">#18858</a>)</li>
<li><a href="https://github.com/microsoft/playwright/commit/8f9bf0f0ac4187c57051673c56c6d80088379a61"><code>8f9bf0f</code></a> cherry-pick(<a href="https://github-redirect.dependabot.com/Microsoft/playwright/issues/18818">#18818</a>): chore: multiply overloaded options in csharp</li>
<li><a href="https://github.com/microsoft/playwright/commit/c3335e4619ae8f4eff9ebabf1416a8f3fe8812ed"><code>c3335e4</code></a> cherry-pick(<a href="https://github-redirect.dependabot.com/Microsoft/playwright/issues/18799">#18799</a>): chore: allow <code>py</code> code blocks for python</li>
<li>Additional commits viewable in <a href="https://github.com/Microsoft/playwright/compare/v1.26.1...v1.28.1">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=playwright&package-manager=npm_and_yarn&previous-version=1.26.1&new-version=1.28.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
Dependabot will merge this PR once it's up-to-date and CI passes on it, as requested by @kopia-builder.

[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

dependabot[bot]: One of your CI runs failed on this pull request, so Dependabot won't merge it.

Dependabot will still automatically merge this pull request if you amend it and your tests pass.
codecov[bot]: # [Codecov](https://codecov.io/gh/kopia/kopia/pull/2615?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) Report
Base: **74.96**% // Head: **74.92**% // Decreases project coverage by **`-0.03%`** :warning:
> Coverage data is based on head [(`638bba5`)](https://codecov.io/gh/kopia/kopia/pull/2615?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) compared to base [(`e57020f`)](https://codecov.io/gh/kopia/kopia/commit/e57020fb70cb717463d0ec1c8bf42a3f1eba98da?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).
> Patch has no changes to coverable lines.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #2615      +/-   ##
==========================================
- Coverage   74.96%   74.92%   -0.04%     
==========================================
  Files         453      453              
  Lines       35541    35541              
==========================================
- Hits        26643    26629      -14     
- Misses       7015     7026      +11     
- Partials     1883     1886       +3     
```


| [Impacted Files](https://codecov.io/gh/kopia/kopia/pull/2615?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) | Coverage Δ | |
|---|---|---|
| [cli/command\_content\_verify.go](https://codecov.io/gh/kopia/kopia/pull/2615/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-Y2xpL2NvbW1hbmRfY29udGVudF92ZXJpZnkuZ28=) | `75.63% <0.00%> (-5.89%)` | :arrow_down: |
| [repo/content/content\_manager\_iterate.go](https://codecov.io/gh/kopia/kopia/pull/2615/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9jb250ZW50L2NvbnRlbnRfbWFuYWdlcl9pdGVyYXRlLmdv) | `88.10% <0.00%> (-2.17%)` | :arrow_down: |
| [internal/cache/persistent\_lru\_cache.go](https://codecov.io/gh/kopia/kopia/pull/2615/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-aW50ZXJuYWwvY2FjaGUvcGVyc2lzdGVudF9scnVfY2FjaGUuZ28=) | `96.06% <0.00%> (-1.75%)` | :arrow_down: |
| [snapshot/snapshotfs/repofs.go](https://codecov.io/gh/kopia/kopia/pull/2615/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-c25hcHNob3Qvc25hcHNob3Rmcy9yZXBvZnMuZ28=) | `79.29% <0.00%> (-1.52%)` | :arrow_down: |
| [repo/content/index/index\_v1.go](https://codecov.io/gh/kopia/kopia/pull/2615/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9jb250ZW50L2luZGV4L2luZGV4X3YxLmdv) | `75.00% <0.00%> (-1.18%)` | :arrow_down: |
| [internal/server/source\_manager.go](https://codecov.io/gh/kopia/kopia/pull/2615/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-aW50ZXJuYWwvc2VydmVyL3NvdXJjZV9tYW5hZ2VyLmdv) | `82.95% <0.00%> (-0.26%)` | :arrow_down: |
| [snapshot/snapshotfs/upload.go](https://codecov.io/gh/kopia/kopia/pull/2615/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-c25hcHNob3Qvc25hcHNob3Rmcy91cGxvYWQuZ28=) | `84.28% <0.00%> (+0.78%)` | :arrow_up: |
| [repo/blob/throttling/throttling\_semaphore.go](https://codecov.io/gh/kopia/kopia/pull/2615/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9ibG9iL3Rocm90dGxpbmcvdGhyb3R0bGluZ19zZW1hcGhvcmUuZ28=) | `100.00% <0.00%> (+2.94%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/kopia/kopia/pull/2615?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).


Patch:
diff --git a/app/package-lock.json b/app/package-lock.json
index d2a969cf..62397d35 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -25,7 +25,7 @@
         "electron": "^19.0.8",
         "electron-builder": "^23.3.3",
         "electron-notarize": "^1.2.2",
-        "playwright": "^1.26.1",
+        "playwright": "^1.28.1",
         "playwright-core": "^1.28.1"
       }
     },
@@ -2825,13 +2825,13 @@
       }
     },
     "node_modules/playwright": {
-      "version": "1.26.1",
-      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.26.1.tgz",
-      "integrity": "sha512-WQmEdCgYYe8jOEkhkW9QLcK0PB+w1RZztBLYIT10MEEsENYg251cU0IzebDINreQsUt+HCwwRhtdz4weH9ICcQ==",
+      "version": "1.28.1",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.28.1.tgz",
+      "integrity": "sha512-92Sz6XBlfHlb9tK5UCDzIFAuIkHHpemA9zwUaqvo+w7sFMSmVMGmvKcbptof/eJObq63PGnMhM75x7qxhTR78Q==",
       "dev": true,
       "hasInstallScript": true,
       "dependencies": {
-        "playwright-core": "1.26.1"
+        "playwright-core": "1.28.1"
       },
       "bin": {
         "playwright": "cli.js"
@@ -2852,18 +2852,6 @@
         "node": ">=14"
       }
     },
-    "node_modules/playwright/node_modules/playwright-core": {
-      "version": "1.26.1",
-      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.26.1.tgz",
-      "integrity": "sha512-hzFchhhxnEiPc4qVPs9q2ZR+5eKNifY2hQDHtg1HnTTUuphYCBP8ZRb2si+B1TR7BHirgXaPi48LIye5SgrLAA==",
-      "dev": true,
-      "bin": {
-        "playwright": "cli.js"
-      },
-      "engines": {
-        "node": ">=14"
-      }
-    },
     "node_modules/plist": {
       "version": "3.0.6",
       "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.6.tgz",
@@ -5932,20 +5920,12 @@
       "optional": true
     },
     "playwright": {
-      "version": "1.26.1",
-      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.26.1.tgz",
-      "integrity": "sha512-WQmEdCgYYe8jOEkhkW9QLcK0PB+w1RZztBLYIT10MEEsENYg251cU0IzebDINreQsUt+HCwwRhtdz4weH9ICcQ==",
+      "version": "1.28.1",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.28.1.tgz",
+      "integrity": "sha512-92Sz6XBlfHlb9tK5UCDzIFAuIkHHpemA9zwUaqvo+w7sFMSmVMGmvKcbptof/eJObq63PGnMhM75x7qxhTR78Q==",
       "dev": true,
       "requires": {
-        "playwright-core": "1.26.1"
-      },
-      "dependencies": {
-        "playwright-core": {
-          "version": "1.26.1",
-          "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.26.1.tgz",
-          "integrity": "sha512-hzFchhhxnEiPc4qVPs9q2ZR+5eKNifY2hQDHtg1HnTTUuphYCBP8ZRb2si+B1TR7BHirgXaPi48LIye5SgrLAA==",
-          "dev": true
-        }
+        "playwright-core": "1.28.1"
       }
     },
     "playwright-core": {
diff --git a/app/package.json b/app/package.json
index 82677f45..6eab894b 100644
--- a/app/package.json
+++ b/app/package.json
@@ -117,7 +117,7 @@
     "electron": "^19.0.8",
     "electron-builder": "^23.3.3",
     "electron-notarize": "^1.2.2",
-    "playwright": "^1.26.1",
+    "playwright": "^1.28.1",
     "playwright-core": "^1.28.1"
   },
   "homepage": "./",
diff --git a/go.mod b/go.mod
index 593f04db..b4bc22cb 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/kopia/kopia
 go 1.19
 
 require (
-	cloud.google.com/go/storage v1.28.0
+	cloud.google.com/go/storage v1.28.1
 	github.com/Azure/azure-pipeline-go v0.2.3 // indirect
 	github.com/Azure/azure-storage-blob-go v0.15.0
 	github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect
@@ -98,7 +98,7 @@ require (
 	go.uber.org/multierr v1.8.0
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c // indirect
+	google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c // indirect
 )
 
 require (
@@ -118,7 +118,7 @@ require (
 require (
 	cloud.google.com/go/compute v1.12.1 // indirect
 	cloud.google.com/go/compute/metadata v0.2.1 // indirect
-	cloud.google.com/go/iam v0.6.0 // indirect
+	cloud.google.com/go/iam v0.7.0 // indirect
 	github.com/alessio/shellescape v1.4.1 // indirect
 	github.com/chromedp/sysutil v1.0.0 // indirect
 	github.com/go-logr/logr v1.2.3 // indirect
diff --git a/go.sum b/go.sum
index 8f38343f..f0bbd5a4 100644
--- a/go.sum
+++ b/go.sum
@@ -27,9 +27,9 @@ cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22m
 cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/iam v0.6.0 h1:nsqQC88kT5Iwlm4MeNGTpfMWddp6NB/UOLFTH6m1QfQ=
-cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc=
-cloud.google.com/go/longrunning v0.1.1 h1:y50CXG4j0+qvEukslYFBCrzaXX0qpFbBzc3PchSu/LE=
+cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs=
+cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=
+cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=
 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
 cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
 cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@@ -39,8 +39,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-cloud.google.com/go/storage v1.28.0 h1:DLrIZ6xkeZX6K70fU/boWx5INJumt6f+nwwWSHXzzGY=
-cloud.google.com/go/storage v1.28.0/go.mod h1:qlgZML35PXA3zoEnIkiPLY4/TOkUleufRlu6qmcf7sI=
+cloud.google.com/go/storage v1.28.1 h1:F5QDG5ChchaAVQhINh24U99OWHURqrW8OmQcGKXcbgI=
+cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
 github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
@@ -721,8 +721,8 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
 google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c h1:QgY/XxIAIeccR+Ca/rDdKubLIU9rcJ3xfy1DC/Wd2Oo=
-google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo=
+google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c h1:S34D59DS2GWOEwWNt4fYmTcFrtlOgukG2k9WsomZ7tg=
+google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
diff --git a/internal/cache/content_cache.go b/internal/cache/content_cache.go
index 74b5a5f0..c3d936eb 100644
--- a/internal/cache/content_cache.go
+++ b/internal/cache/content_cache.go
@@ -5,6 +5,7 @@
 
 	"github.com/pkg/errors"
 
+	"github.com/kopia/kopia/internal/cacheprot"
 	"github.com/kopia/kopia/internal/gather"
 	"github.com/kopia/kopia/internal/impossible"
 	"github.com/kopia/kopia/internal/metrics"
@@ -189,7 +190,7 @@ func NewContentCache(ctx context.Context, st blob.Storage, opt Options, mr *metr
 		}
 	}
 
-	pc, err := NewPersistentCache(ctx, opt.CacheSubDir, cacheStorage, ChecksumProtection(opt.HMACSecret), opt.Sweep, mr)
+	pc, err := NewPersistentCache(ctx, opt.CacheSubDir, cacheStorage, cacheprot.ChecksumProtection(opt.HMACSecret), opt.Sweep, mr)
 	if err != nil {
 		return nil, errors.Wrap(err, "unable to create base cache")
 	}
diff --git a/internal/cache/persistent_lru_cache.go b/internal/cache/persistent_lru_cache.go
index 6b539fab..09d0f8af 100644
--- a/internal/cache/persistent_lru_cache.go
+++ b/internal/cache/persistent_lru_cache.go
@@ -11,6 +11,7 @@
 	lru "github.com/hashicorp/golang-lru"
 	"github.com/pkg/errors"
 
+	"github.com/kopia/kopia/internal/cacheprot"
 	"github.com/kopia/kopia/internal/clock"
 	"github.com/kopia/kopia/internal/ctxutil"
 	"github.com/kopia/kopia/internal/gather"
@@ -42,7 +43,7 @@ type PersistentCache struct {
 	anyChange atomic.Bool
 
 	cacheStorage      Storage
-	storageProtection StorageProtection
+	storageProtection cacheprot.StorageProtection
 	sweep             SweepSettings
 
 	description string
@@ -137,7 +138,7 @@ func (c *PersistentCache) GetPartial(ctx context.Context, key string, offset, le
 		prot := c.storageProtection
 		if length >= 0 {
 			// only full items have protection.
-			prot = nullStorageProtection{}
+			prot = cacheprot.NoProtection()
 		}
 
 		if err := prot.Verify(key, tmp.Bytes(), output); err == nil {
@@ -333,7 +334,7 @@ func (s SweepSettings) applyDefaults() SweepSettings {
 }
 
 // NewPersistentCache creates the persistent cache in the provided storage.
-func NewPersistentCache(ctx context.Context, description string, cacheStorage Storage, storageProtection StorageProtection, sweep SweepSettings, mr *metrics.Registry) (*PersistentCache, error) {
+func NewPersistentCache(ctx context.Context, description string, cacheStorage Storage, storageProtection cacheprot.StorageProtection, sweep SweepSettings, mr *metrics.Registry) (*PersistentCache, error) {
 	if cacheStorage == nil {
 		return nil, nil
 	}
@@ -341,7 +342,7 @@ func NewPersistentCache(ctx context.Context, description string, cacheStorage St
 	sweep = sweep.applyDefaults()
 
 	if storageProtection == nil {
-		storageProtection = NoProtection()
+		storageProtection = cacheprot.NoProtection()
 	}
 
 	c := &PersistentCache{
diff --git a/internal/cache/storage_protection.go b/internal/cacheprot/storage_protection.go
similarity index 97%
rename from internal/cache/storage_protection.go
rename to internal/cacheprot/storage_protection.go
index 947a45c7..77658215 100644
--- a/internal/cache/storage_protection.go
+++ b/internal/cacheprot/storage_protection.go
@@ -1,4 +1,5 @@
-package cache
+// Package cacheprot provides utilities for protection of cache entries.
+package cacheprot
 
 import (
 	"crypto/sha256"
diff --git a/repo/api_server_repository.go b/repo/api_server_repository.go
index 7f4c68d3..520dbc9d 100644
--- a/repo/api_server_repository.go
+++ b/repo/api_server_repository.go
@@ -35,7 +35,7 @@ type apiServerRepository struct {
 	omgr                             *object.Manager
 	wso                              WriteSessionOptions
 
-	immutableServerRepositoryParameters // immutable parameters
+	*immutableServerRepositoryParameters // immutable parameters
 }
 
 func (r *apiServerRepository) APIServerURL() string {
@@ -132,7 +132,19 @@ func (r *apiServerRepository) Refresh(ctx context.Context) error {
 }
 
 func (r *apiServerRepository) Flush(ctx context.Context) error {
-	return errors.Wrap(r.cli.Post(ctx, "flush", nil, nil), "Flush")
+	if err := invokeCallbacks(ctx, r, r.beforeFlush); err != nil {
+		return errors.Wrap(err, "before flush")
+	}
+
+	if err := r.cli.Post(ctx, "flush", nil, nil); err != nil {
+		return errors.Wrap(err, "Flush")
+	}
+
+	if err := invokeCallbacks(ctx, r, r.afterFlush); err != nil {
+		return errors.Wrap(err, "after flush")
+	}
+
+	return nil
 }
 
 func (r *apiServerRepository) SupportsContentCompression() (bool, error) {
@@ -153,6 +165,10 @@ func (r *apiServerRepository) NewWriter(ctx context.Context, opt WriteSessionOpt
 	w.omgr = omgr
 	w.wso = opt
 
+	if w.wso.OnUpload == nil {
+		w.wso.OnUpload = func(i int64) {}
+	}
+
 	r.addRef()
 
 	return ctx, w, nil
@@ -258,7 +274,7 @@ func (r *apiServerRepository) PrefetchContents(ctx context.Context, contentIDs [
 var _ Repository = (*apiServerRepository)(nil)
 
 // openRestAPIRepository connects remote repository over Kopia API.
-func openRestAPIRepository(ctx context.Context, si *APIServerInfo, password string, par immutableServerRepositoryParameters) (Repository, error) {
+func openRestAPIRepository(ctx context.Context, si *APIServerInfo, password string, par *immutableServerRepositoryParameters) (Repository, error) {
 	cli, err := apiclient.NewKopiaAPIClient(apiclient.Options{
 		BaseURL:                             si.BaseURL,
 		TrustedServerCertificateFingerprint: si.TrustedServerCertificateFingerprint,
diff --git a/repo/content/committed_read_manager.go b/repo/content/committed_read_manager.go
index 9800bf82..1aa99fa7 100644
--- a/repo/content/committed_read_manager.go
+++ b/repo/content/committed_read_manager.go
@@ -11,6 +11,7 @@
 	"go.uber.org/zap"
 
 	"github.com/kopia/kopia/internal/cache"
+	"github.com/kopia/kopia/internal/cacheprot"
 	"github.com/kopia/kopia/internal/clock"
 	"github.com/kopia/kopia/internal/epoch"
 	"github.com/kopia/kopia/internal/gather"
@@ -427,7 +428,7 @@ func (sm *SharedManager) setupReadManagerCaches(ctx context.Context, caching *Ca
 		return errors.Wrap(err, "unable to initialize index blob cache storage")
 	}
 
-	indexBlobCache, err := cache.NewPersistentCache(ctx, "index-blobs", indexBlobStorage, cache.ChecksumProtection(caching.HMACSecret), cache.SweepSettings{
+	indexBlobCache, err := cache.NewPersistentCache(ctx, "index-blobs", indexBlobStorage, cacheprot.ChecksumProtection(caching.HMACSecret), cache.SweepSettings{
 		MaxSizeBytes: metadataCacheSize,
 		MinSweepAge:  caching.MinMetadataSweepAge.DurationOrDefault(DefaultMetadataCacheSweepAge),
 	}, mr)
diff --git a/repo/grpc_repository_client.go b/repo/grpc_repository_client.go
index f3bdf9a9..20994a84 100644
--- a/repo/grpc_repository_client.go
+++ b/repo/grpc_repository_client.go
@@ -73,7 +73,7 @@ type grpcRepositoryClient struct {
 
 	asyncWritesWG *errgroup.Group
 
-	immutableServerRepositoryParameters
+	*immutableServerRepositoryParameters
 
 	serverSupportsContentCompression bool
 	omgr                             *object.Manager
@@ -422,11 +422,21 @@ func (r *grpcRepositoryClient) Flush(ctx context.Context) error {
 		return errors.Wrap(err, "error waiting for async writes")
 	}
 
-	_, err := inSessionWithoutRetry(ctx, r, func(ctx context.Context, sess *grpcInnerSession) (bool, error) {
+	if err := invokeCallbacks(ctx, r, r.beforeFlush); err != nil {
+		return errors.Wrap(err, "before flush")
+	}
+
+	if _, err := inSessionWithoutRetry(ctx, r, func(ctx context.Context, sess *grpcInnerSession) (bool, error) {
 		return false, sess.Flush(ctx)
-	})
+	}); err != nil {
+		return err
+	}
 
-	return err
+	if err := invokeCallbacks(ctx, r, r.afterFlush); err != nil {
+		return errors.Wrap(err, "after flush")
+	}
+
+	return nil
 }
 
 func (r *grpcInnerSession) Flush(ctx context.Context) error {
@@ -743,7 +753,7 @@ func (c grpcCreds) RequireTransportSecurity() bool {
 
 // openGRPCAPIRepository opens the Repository based on remote GRPC server.
 // The APIServerInfo must have the address of the repository as 'https://host:port'
-func openGRPCAPIRepository(ctx context.Context, si *APIServerInfo, password string, par immutableServerRepositoryParameters) (Repository, error) {
+func openGRPCAPIRepository(ctx context.Context, si *APIServerInfo, password string, par *immutableServerRepositoryParameters) (Repository, error) {
 	var transportCreds credentials.TransportCredentials
 
 	if si.TrustedServerCertificateFingerprint != "" {
@@ -852,7 +862,7 @@ func newGRPCAPIRepositoryForConnection(
 	conn *grpc.ClientConn,
 	opt WriteSessionOptions,
 	transparentRetries bool,
-	par immutableServerRepositoryParameters,
+	par *immutableServerRepositoryParameters,
 ) (*grpcRepositoryClient, error) {
 	if opt.OnUpload == nil {
 		opt.OnUpload = func(i int64) {}
diff --git a/repo/open.go b/repo/open.go
index d44e73cf..888325f1 100644
--- a/repo/open.go
+++ b/repo/open.go
@@ -13,6 +13,7 @@
 	"golang.org/x/crypto/scrypt"
 
 	"github.com/kopia/kopia/internal/cache"
+	"github.com/kopia/kopia/internal/cacheprot"
 	"github.com/kopia/kopia/internal/epoch"
 	"github.com/kopia/kopia/internal/feature"
 	"github.com/kopia/kopia/internal/metrics"
@@ -64,11 +65,13 @@
 
 // Options provides configuration parameters for connection to a repository.
 type Options struct {
-	TraceStorage        bool             // Logs all storage access using provided Printf-style function
-	TimeNowFunc         func() time.Time // Time provider
-	DisableInternalLog  bool             // Disable internal log
-	UpgradeOwnerID      string           // Owner-ID of any upgrade in progress, when this is not set the access may be restricted
-	DoNotWaitForUpgrade bool             // Disable the exponential forever backoff on an upgrade lock.
+	TraceStorage        bool                        // Logs all storage access using provided Printf-style function
+	TimeNowFunc         func() time.Time            // Time provider
+	DisableInternalLog  bool                        // Disable internal log
+	UpgradeOwnerID      string                      // Owner-ID of any upgrade in progress, when this is not set the access may be restricted
+	DoNotWaitForUpgrade bool                        // Disable the exponential forever backoff on an upgrade lock.
+	BeforeFlush         []RepositoryWriterCallbacks // list of callbacks to invoke before every flush
+	AfterFlush          []RepositoryWriterCallbacks // list of callbacks to invoke after every flush
 
 	OnFatalError func(err error) // function to invoke when repository encounters a fatal error, usually invokes os.Exit
 
@@ -119,7 +122,7 @@ func Open(ctx context.Context, configFile, password string, options *Options) (r
 	}
 
 	if lc.APIServer != nil {
-		return OpenAPIServer(ctx, lc.APIServer, lc.ClientOptions, lc.Caching, password)
+		return openAPIServer(ctx, lc.APIServer, lc.ClientOptions, lc.Caching, password, options)
 	}
 
 	return openDirect(ctx, configFile, lc, password, options)
@@ -143,7 +146,7 @@ func getContentCacheOrNil(ctx context.Context, opt *content.CachingOptions, pass
 		return nil, errors.Wrap(err, "unable to derive cache encryption key from password")
 	}
 
-	prot, err := cache.AuthenticatedEncryptionProtection(cacheEncryptionKey)
+	prot, err := cacheprot.AuthenticatedEncryptionProtection(cacheEncryptionKey)
 	if err != nil {
 		return nil, errors.Wrap(err, "unable to initialize protection")
 	}
@@ -159,8 +162,10 @@ func getContentCacheOrNil(ctx context.Context, opt *content.CachingOptions, pass
 	return pc, nil
 }
 
-// OpenAPIServer connects remote repository over Kopia API.
-func OpenAPIServer(ctx context.Context, si *APIServerInfo, cliOpts ClientOptions, cachingOptions *content.CachingOptions, password string) (Repository, error) {
+// openAPIServer connects remote repository over Kopia API.
+func openAPIServer(ctx context.Context, si *APIServerInfo, cliOpts ClientOptions, cachingOptions *content.CachingOptions, password string, options *Options) (Repository, error) {
+	cachingOptions = cachingOptions.CloneOrDefault()
+
 	mr := metrics.NewRegistry()
 
 	contentCache, err := getContentCacheOrNil(ctx, cachingOptions, password, mr)
@@ -179,11 +184,13 @@ func(ctx context.Context) error {
 		mr.Close,
 	)
 
-	par := immutableServerRepositoryParameters{
+	par := &immutableServerRepositoryParameters{
 		cliOpts:          cliOpts,
 		contentCache:     contentCache,
 		metricsRegistry:  mr,
 		refCountedCloser: closer,
+		beforeFlush:      options.BeforeFlush,
+		afterFlush:       options.AfterFlush,
 	}
 
 	if si.DisableGRPC {
@@ -345,6 +352,8 @@ func openWithConfig(ctx context.Context, st blob.Storage, cliOpts ClientOptions,
 			throttler:        throttler,
 			metricsRegistry:  mr,
 			refCountedCloser: closer,
+			beforeFlush:      options.BeforeFlush,
+			afterFlush:       options.AfterFlush,
 		},
 	}
 
diff --git a/repo/repository.go b/repo/repository.go
index ad60b847..58105eff 100644
--- a/repo/repository.go
+++ b/repo/repository.go
@@ -97,10 +97,25 @@ type immutableDirectRepositoryParameters struct {
 	nextWriterID    *int32
 	throttler       throttling.SettableThrottler
 	metricsRegistry *metrics.Registry
+	beforeFlush     []RepositoryWriterCallbacks
+	afterFlush      []RepositoryWriterCallbacks
 
 	*refCountedCloser
 }
 
+// RepositoryWriterCallbacks is a hook function invoked before and after each flush.
+type RepositoryWriterCallbacks func(ctx context.Context, w RepositoryWriter) error
+
+func invokeCallbacks(ctx context.Context, w RepositoryWriter, callbacks []RepositoryWriterCallbacks) error {
+	for _, h := range callbacks {
+		if err := h(ctx, w); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 // directRepository is an implementation of repository that directly manipulates underlying storage.
 type directRepository struct {
 	immutableDirectRepositoryParameters
@@ -277,11 +292,23 @@ func (r *directRepository) NewDirectWriter(ctx context.Context, opt WriteSession
 
 // Flush waits for all in-flight writes to complete.
 func (r *directRepository) Flush(ctx context.Context) error {
+	if err := invokeCallbacks(ctx, r, r.beforeFlush); err != nil {
+		return errors.Wrap(err, "before flush")
+	}
+
 	if err := r.mmgr.Flush(ctx); err != nil {
 		return errors.Wrap(err, "error flushing manifests")
 	}
 
-	return errors.Wrap(r.cmgr.Flush(ctx), "error flushing contents")
+	if err := r.cmgr.Flush(ctx); err != nil {
+		return errors.Wrap(err, "error flushing contents")
+	}
+
+	if err := invokeCallbacks(ctx, r, r.afterFlush); err != nil {
+		return errors.Wrap(err, "after flush")
+	}
+
+	return nil
 }
 
 // Metrics provides access to metrics registry.
diff --git a/repo/server_repository_params.go b/repo/server_repository_params.go
index cd870d37..5cb7e8f1 100644
--- a/repo/server_repository_params.go
+++ b/repo/server_repository_params.go
@@ -14,6 +14,9 @@ type immutableServerRepositoryParameters struct {
 	cliOpts         ClientOptions
 	metricsRegistry *metrics.Registry
 	contentCache    *cache.PersistentCache
+	beforeFlush     []RepositoryWriterCallbacks
+	afterFlush      []RepositoryWriterCallbacks
+
 	*refCountedCloser
 }
 


Test patch:
diff --git a/internal/cache/persistent_lru_cache_test.go b/internal/cache/persistent_lru_cache_test.go
index 24dec137..76ee7ee5 100644
--- a/internal/cache/persistent_lru_cache_test.go
+++ b/internal/cache/persistent_lru_cache_test.go
@@ -11,6 +11,7 @@
 
 	"github.com/kopia/kopia/internal/blobtesting"
 	"github.com/kopia/kopia/internal/cache"
+	"github.com/kopia/kopia/internal/cacheprot"
 	"github.com/kopia/kopia/internal/fault"
 	"github.com/kopia/kopia/internal/gather"
 	"github.com/kopia/kopia/internal/testlogging"
@@ -25,7 +26,7 @@ func TestPersistentLRUCache(t *testing.T) {
 
 	cs := blobtesting.NewMapStorage(blobtesting.DataMap{}, nil, nil).(cache.Storage)
 
-	pc, err := cache.NewPersistentCache(ctx, "testing", cs, cache.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
+	pc, err := cache.NewPersistentCache(ctx, "testing", cs, cacheprot.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
 		MaxSizeBytes:   maxSizeBytes,
 		TouchThreshold: cache.DefaultTouchThreshold,
 		SweepFrequency: cache.DefaultSweepFrequency,
@@ -67,7 +68,7 @@ func TestPersistentLRUCache(t *testing.T) {
 	verifyBlobExists(ctx, t, cs, "key3")
 	verifyBlobExists(ctx, t, cs, "key4")
 
-	pc, err = cache.NewPersistentCache(ctx, "testing", cs, cache.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
+	pc, err = cache.NewPersistentCache(ctx, "testing", cs, cacheprot.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
 		MaxSizeBytes:   maxSizeBytes,
 		TouchThreshold: cache.DefaultTouchThreshold,
 		SweepFrequency: cache.DefaultSweepFrequency,
@@ -81,7 +82,7 @@ func TestPersistentLRUCache(t *testing.T) {
 
 	// create another persistent cache based on the same storage but wrong protection key.
 	// all reads from cache will be invalid, which means GetOrLoad will fetch them from the source.
-	pc2, err := cache.NewPersistentCache(ctx, "testing", cs, cache.ChecksumProtection([]byte{3, 2, 1}), cache.SweepSettings{
+	pc2, err := cache.NewPersistentCache(ctx, "testing", cs, cacheprot.ChecksumProtection([]byte{3, 2, 1}), cache.SweepSettings{
 		MaxSizeBytes:   maxSizeBytes,
 		TouchThreshold: cache.DefaultTouchThreshold,
 		SweepFrequency: cache.DefaultSweepFrequency,
@@ -153,7 +154,7 @@ func TestPersistentLRUCache_GetDeletesInvalidBlob(t *testing.T) {
 	fs := blobtesting.NewFaultyStorage(st)
 	fc := faultyCache{fs}
 
-	pc, err := cache.NewPersistentCache(ctx, "test", fc, cache.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{}, nil)
+	pc, err := cache.NewPersistentCache(ctx, "test", fc, cacheprot.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{}, nil)
 	require.NoError(t, err)
 
 	pc.Put(ctx, "key", gather.FromSlice([]byte{1, 2, 3}))
@@ -183,7 +184,7 @@ func TestPersistentLRUCache_PutIgnoresStorageFailure(t *testing.T) {
 	fs := blobtesting.NewFaultyStorage(st)
 	fc := faultyCache{fs}
 
-	pc, err := cache.NewPersistentCache(ctx, "test", fc, cache.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{}, nil)
+	pc, err := cache.NewPersistentCache(ctx, "test", fc, cacheprot.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{}, nil)
 	require.NoError(t, err)
 
 	fs.AddFault(blobtesting.MethodPutBlob).ErrorInstead(someError)
@@ -209,7 +210,7 @@ func TestPersistentLRUCache_SweepMinSweepAge(t *testing.T) {
 	fs := blobtesting.NewFaultyStorage(st)
 	fc := faultyCache{fs}
 
-	pc, err := cache.NewPersistentCache(ctx, "test", fc, cache.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
+	pc, err := cache.NewPersistentCache(ctx, "test", fc, cacheprot.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
 		SweepFrequency: 100 * time.Millisecond,
 		MaxSizeBytes:   1000,
 		MinSweepAge:    10 * time.Second,
@@ -238,7 +239,7 @@ func TestPersistentLRUCache_SweepIgnoresErrors(t *testing.T) {
 	fs := blobtesting.NewFaultyStorage(st)
 	fc := faultyCache{fs}
 
-	pc, err := cache.NewPersistentCache(ctx, "test", fc, cache.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
+	pc, err := cache.NewPersistentCache(ctx, "test", fc, cacheprot.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
 		SweepFrequency: 100 * time.Millisecond,
 		MaxSizeBytes:   1000,
 	}, nil)
@@ -271,7 +272,7 @@ func TestPersistentLRUCache_Sweep1(t *testing.T) {
 	fs := blobtesting.NewFaultyStorage(st)
 	fc := faultyCache{fs}
 
-	pc, err := cache.NewPersistentCache(ctx, "test", fc, cache.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
+	pc, err := cache.NewPersistentCache(ctx, "test", fc, cacheprot.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
 		SweepFrequency: 10 * time.Millisecond,
 		MaxSizeBytes:   1,
 		MinSweepAge:    0 * time.Second,
diff --git a/internal/cache/storage_protection_test.go b/internal/cacheprot/storage_protection_test.go
similarity index 76%
rename from internal/cache/storage_protection_test.go
rename to internal/cacheprot/storage_protection_test.go
index 4d57fce0..31d5ee11 100644
--- a/internal/cache/storage_protection_test.go
+++ b/internal/cacheprot/storage_protection_test.go
@@ -1,4 +1,4 @@
-package cache_test
+package cacheprot_test
 
 import (
 	"bytes"
@@ -6,27 +6,27 @@
 
 	"github.com/stretchr/testify/require"
 
-	"github.com/kopia/kopia/internal/cache"
+	"github.com/kopia/kopia/internal/cacheprot"
 	"github.com/kopia/kopia/internal/gather"
 )
 
 func TestNoStorageProection(t *testing.T) {
-	testStorageProtection(t, cache.NoProtection(), false)
+	testStorageProtection(t, cacheprot.NoProtection(), false)
 }
 
 func TestHMACStorageProtection(t *testing.T) {
-	testStorageProtection(t, cache.ChecksumProtection([]byte{1, 2, 3, 4}), true)
+	testStorageProtection(t, cacheprot.ChecksumProtection([]byte{1, 2, 3, 4}), true)
 }
 
 func TestEncryptionStorageProtection(t *testing.T) {
-	e, err := cache.AuthenticatedEncryptionProtection([]byte{1})
+	e, err := cacheprot.AuthenticatedEncryptionProtection([]byte{1})
 	require.NoError(t, err)
 
 	testStorageProtection(t, e, true)
 }
 
 //nolint:thelper
-func testStorageProtection(t *testing.T, sp cache.StorageProtection, protectsFromBitFlips bool) {
+func testStorageProtection(t *testing.T, sp cacheprot.StorageProtection, protectsFromBitFlips bool) {
 	payload := []byte{0, 1, 2, 3, 4}
 
 	var protected gather.WriteBuffer
diff --git a/repo/repository_test.go b/repo/repository_test.go
index 40ca7f11..37f46255 100644
--- a/repo/repository_test.go
+++ b/repo/repository_test.go
@@ -7,6 +7,7 @@
 	"math/rand"
 	"runtime/debug"
 	"strings"
+	"sync/atomic"
 	"testing"
 	"time"
 
@@ -17,7 +18,9 @@
 	"github.com/kopia/kopia/internal/epoch"
 	"github.com/kopia/kopia/internal/gather"
 	"github.com/kopia/kopia/internal/repotesting"
+	"github.com/kopia/kopia/internal/servertesting"
 	"github.com/kopia/kopia/internal/testlogging"
+	"github.com/kopia/kopia/internal/testutil"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/blob"
 	"github.com/kopia/kopia/repo/blob/beforeop"
@@ -583,8 +586,22 @@ func TestObjectWritesWithRetention(t *testing.T) {
 	}))
 }
 
-func (s *formatSpecificTestSuite) TestWriteSessionFlushOnSuccess(t *testing.T) {
-	ctx, env := repotesting.NewEnvironment(t, s.formatVersion)
+func TestWriteSessionFlushOnSuccess(t *testing.T) {
+	var beforeFlushCount, afterFlushCount atomic.Int32
+
+	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant, repotesting.Options{
+		OpenOptions: func(o *repo.Options) {
+			o.BeforeFlush = append(o.BeforeFlush, func(ctx context.Context, w repo.RepositoryWriter) error {
+				beforeFlushCount.Add(1)
+				return nil
+			})
+
+			o.AfterFlush = append(o.AfterFlush, func(ctx context.Context, w repo.RepositoryWriter) error {
+				afterFlushCount.Add(1)
+				return nil
+			})
+		},
+	})
 
 	var oid object.ID
 
@@ -593,7 +610,109 @@ func (s *formatSpecificTestSuite) TestWriteSessionFlushOnSuccess(t *testing.T) {
 		return nil
 	}))
 
+	require.EqualValues(t, 1, beforeFlushCount.Load())
+	require.EqualValues(t, 1, afterFlushCount.Load())
+
 	verify(ctx, t, env.Repository, oid, []byte{1, 2, 3}, "test-1")
+
+	someErr := errors.Errorf("some error")
+
+	require.ErrorIs(t, repo.WriteSession(ctx, env.Repository, repo.WriteSessionOptions{}, func(ctx context.Context, w repo.RepositoryWriter) error {
+		oid = writeObject(ctx, t, w, []byte{1, 2, 3, 4}, "test-2")
+		return someErr
+	}), someErr)
+
+	require.EqualValues(t, 1, beforeFlushCount.Load())
+	require.EqualValues(t, 1, afterFlushCount.Load())
+
+	require.ErrorIs(t, repo.WriteSession(ctx, env.Repository, repo.WriteSessionOptions{
+		FlushOnFailure: true,
+	}, func(ctx context.Context, w repo.RepositoryWriter) error {
+		oid = writeObject(ctx, t, w, []byte{1, 2, 3, 4, 5}, "test-3")
+		return someErr
+	}), someErr)
+
+	require.EqualValues(t, 2, beforeFlushCount.Load())
+	require.EqualValues(t, 2, afterFlushCount.Load())
+}
+
+func TestWriteSessionFlushOnSuccessClient_REST(t *testing.T) {
+	testWriteSessionFlushOnSuccessClient(t, true)
+}
+
+func TestWriteSessionFlushOnSuccessClient_GRPC(t *testing.T) {
+	testWriteSessionFlushOnSuccessClient(t, false)
+}
+
+//nolint:thelper
+func testWriteSessionFlushOnSuccessClient(t *testing.T, disableGRPC bool) {
+	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant, repotesting.Options{})
+
+	apiServerInfo := servertesting.StartServer(t, env, true)
+	apiServerInfo.DisableGRPC = disableGRPC
+
+	var beforeFlushCount, afterFlushCount atomic.Int32
+
+	ctx2, cancel := context.WithCancel(testlogging.Context(t))
+	defer cancel()
+
+	rep, err := servertesting.ConnectAndOpenAPIServer(t, ctx2, apiServerInfo, repo.ClientOptions{
+		Username: servertesting.TestUsername,
+		Hostname: servertesting.TestHostname,
+	}, content.CachingOptions{
+		CacheDirectory: testutil.TempDirectory(t),
+	}, servertesting.TestPassword, &repo.Options{
+		BeforeFlush: []repo.RepositoryWriterCallbacks{
+			func(ctx context.Context, w repo.RepositoryWriter) error {
+				beforeFlushCount.Add(1)
+				return nil
+			},
+		},
+		AfterFlush: []repo.RepositoryWriterCallbacks{
+			func(ctx context.Context, w repo.RepositoryWriter) error {
+				afterFlushCount.Add(1)
+				return nil
+			},
+		},
+	})
+
+	defer rep.Close(ctx) //nolint:errcheck,staticcheck
+
+	require.NoError(t, err)
+
+	var oid object.ID
+
+	require.NoError(t, repo.WriteSession(ctx, rep, repo.WriteSessionOptions{}, func(ctx context.Context, w repo.RepositoryWriter) error {
+		oid = writeObject(ctx, t, w, []byte{1, 2, 3}, "test-1")
+		return nil
+	}))
+
+	require.EqualValues(t, 1, beforeFlushCount.Load())
+	require.EqualValues(t, 1, afterFlushCount.Load())
+
+	verify(ctx, t, rep, oid, []byte{1, 2, 3}, "test-1")
+
+	someErr := errors.Errorf("some error")
+
+	require.ErrorIs(t, repo.WriteSession(ctx, rep, repo.WriteSessionOptions{}, func(ctx context.Context, w repo.RepositoryWriter) error {
+		oid = writeObject(ctx, t, w, []byte{1, 2, 3, 4}, "test-2")
+		return someErr
+	}), someErr)
+
+	require.EqualValues(t, 1, beforeFlushCount.Load())
+	require.EqualValues(t, 1, afterFlushCount.Load())
+
+	require.ErrorIs(t, repo.WriteSession(ctx, rep, repo.WriteSessionOptions{
+		FlushOnFailure: true,
+	}, func(ctx context.Context, w repo.RepositoryWriter) error {
+		oid = writeObject(ctx, t, w, []byte{1, 2, 3, 4, 5}, "test-3")
+		return someErr
+	}), someErr)
+
+	t.Logf("-----")
+
+	require.EqualValues(t, 2, beforeFlushCount.Load())
+	require.EqualValues(t, 2, afterFlushCount.Load())
 }
 
 func (s *formatSpecificTestSuite) TestWriteSessionNoFlushOnFailure(t *testing.T) {