Repository: kopia/kopia
Pull request #3216 (2023-08-13 22:48:28)

feat(server): add server support for unix domain sockets
Enable using unix domain sockets with kopia server.

unix sockets can be useful to allow connecting services without going throgh the network interface.  They tend to be faster than using tcp sockets, and can be restricted via file-access permissions.

I'm using unix sockets in a container solution using rootless containers with networking disabled via podman + systemd socket-activated-sockets.  This means that the reverse-proxy (nginx) doesn't need any network interface, as it speaks to the outside world through a socket-activated-socket, and speaks to other services via unix sockets.

The patch also implements the ability to use unixsockets for client connections, but  I'm not sure what the use of that is other than enabling testing



jkowalski: This is a great idea, the PR looks very good too, please fix all the failures reported by automatic checkers (and likely also disable the test on Windows) and we can merge it.
PhracturedBlue: Hopefully the tests all pass  now
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/kopia/kopia/pull/3216?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) Report
Patch coverage: **`86.11%`** and project coverage change: **`-0.03%`** :warning:
> Comparison is base [(`ad0b370`)](https://app.codecov.io/gh/kopia/kopia/commit/ad0b370c49dfa3689f888200699810a19ea59f31?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) 75.45% compared to head [(`eaab39b`)](https://app.codecov.io/gh/kopia/kopia/pull/3216?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) 75.43%.
> Report is 1 commits behind head on master.

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


```diff
@@            Coverage Diff             @@
##           master    #3216      +/-   ##
==========================================
- Coverage   75.45%   75.43%   -0.03%     
==========================================
  Files         459      459              
  Lines       36498    36526      +28     
==========================================
+ Hits        27541    27553      +12     
- Misses       7044     7053       +9     
- Partials     1913     1920       +7     
```


| [Files Changed](https://app.codecov.io/gh/kopia/kopia/pull/3216?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) | Coverage Δ | |
|---|---|---|
| [repo/grpc\_repository\_client.go](https://app.codecov.io/gh/kopia/kopia/pull/3216?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9ncnBjX3JlcG9zaXRvcnlfY2xpZW50Lmdv) | `79.83% <28.57%> (-0.37%)` | :arrow_down: |
| [cli/command\_server\_tls.go](https://app.codecov.io/gh/kopia/kopia/pull/3216?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-Y2xpL2NvbW1hbmRfc2VydmVyX3Rscy5nbw==) | `68.75% <100.00%> (+3.75%)` | :arrow_up: |
| [internal/apiclient/apiclient.go](https://app.codecov.io/gh/kopia/kopia/pull/3216?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-aW50ZXJuYWwvYXBpY2xpZW50L2FwaWNsaWVudC5nbw==) | `82.85% <100.00%> (+1.60%)` | :arrow_up: |

... and [3 files with indirect coverage changes](https://app.codecov.io/gh/kopia/kopia/pull/3216/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/kopia/kopia/pull/3216?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).

PhracturedBlue: This test runs fine on debian when I run 'make test'   It appears that there is no address given for --address when I look at the test results, but I have no idea how to reproduce this
PhracturedBlue: Hopefully 3rd time is the charm.  The last issue appears to be that unix sockets must be < 108 (104 on Mac) bytes long.  and the tmpdir was too long.  I wasn't previously running with ENABLE_LONG_FILENAMES=1 so I missed it.

Patch:
diff --git a/.github/workflows/make.yml b/.github/workflows/make.yml
index d3dddbde..1b9c827b 100644
--- a/.github/workflows/make.yml
+++ b/.github/workflows/make.yml
@@ -84,9 +84,10 @@ jobs:
       run: make ci-build
       timeout-minutes: 40
       env:
-        # Apple ID and app-specific password for notarizaton, used by Electron Builder
-        APPLEID: ${{ secrets.APPLEID }}
-        APPLEIDPASS: ${{ secrets.APPLEIDPASS }}
+        # Apple credentials for notarizaton, used by Electron Builder
+        APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
+        APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
+        APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }}
         KOPIA_UI_NOTARIZE: ${{ secrets.KOPIA_UI_NOTARIZE }}
 
         # tool to install Windows signing certificate
diff --git a/app/notarize.js b/app/notarize.js
index 5eff1cfd..c5bb2979 100644
--- a/app/notarize.js
+++ b/app/notarize.js
@@ -1,5 +1,7 @@
 require('dotenv').config();
 const { notarize } = require('@electron/notarize');
+const fs = require('fs');
+const crypto = require('crypto');
 
 exports.default = async function notarizing(context) {
   const { electronPlatformName, appOutDir } = context;  
@@ -14,14 +16,23 @@ exports.default = async function notarizing(context) {
 
   const appName = context.packager.appInfo.productFilename;
 
-  console.log('Submitting app for Apple notarization...')
-  let timerId = setInterval(() => { console.log('Still waiting for notarization response...') }, 30000);
-  let x =  await notarize({
-    appBundleId: 'io.kopia.ui',
-    appPath: `${appOutDir}/${appName}.app`,
-    appleId: process.env.APPLEID,
-    appleIdPassword: process.env.APPLEIDPASS,
-  });
-  clearTimeout(timerId);
-  return x;
+  const apiKey = Buffer.from(process.env.APPLE_API_KEY_BASE64, 'base64').toString('ascii');
+  const keyFile = "/tmp/key-" +crypto.randomBytes(32).toString('hex');
+
+  try {
+    fs.writeFileSync(keyFile, apiKey);
+    console.log('Submitting app for Apple notarization...')
+    let timerId = setInterval(() => { console.log('Still waiting for notarization response...') }, 30000);
+    let x =  await notarize({
+      appBundleId: 'io.kopia.ui',
+      appPath: `${appOutDir}/${appName}.app`,
+      appleApiIssuer: process.env.APPLE_API_ISSUER,
+      appleApiKeyId: process.env.APPLE_API_KEY_ID,
+      appleApiKey: keyFile,
+    });
+    clearTimeout(timerId);
+    return x;
+  } finally {
+    fs.unlinkSync(keyFile);
+  }
 };
\ No newline at end of file
diff --git a/cli/command_server_tls.go b/cli/command_server_tls.go
index a6c3dd54..8e1e4ee2 100644
--- a/cli/command_server_tls.go
+++ b/cli/command_server_tls.go
@@ -14,6 +14,7 @@
 	"net"
 	"net/http"
 	"os"
+	"strings"
 	"time"
 
 	"github.com/pkg/errors"
@@ -34,7 +35,16 @@ func (c *commandServerStart) generateServerCertificate(ctx context.Context) (*x5
 }
 
 func (c *commandServerStart) startServerWithOptionalTLS(ctx context.Context, httpServer *http.Server) error {
-	l, err := net.Listen("tcp", httpServer.Addr)
+	var l net.Listener
+
+	var err error
+
+	if strings.HasPrefix(httpServer.Addr, "unix:") {
+		l, err = net.Listen("unix", strings.TrimPrefix(httpServer.Addr, "unix:"))
+	} else {
+		l, err = net.Listen("tcp", httpServer.Addr)
+	}
+
 	if err != nil {
 		return errors.Wrap(err, "listen error")
 	}
@@ -86,10 +96,15 @@ func (c *commandServerStart) startServerWithOptionalTLSAndListener(ctx context.C
 		return err
 	}
 
+	udsPfx := ""
+	if listener.Addr().Network() == "unix" {
+		udsPfx = "unix+"
+	}
+
 	switch {
 	case c.serverStartTLSCertFile != "" && c.serverStartTLSKeyFile != "":
 		// PEM files provided
-		fmt.Fprintf(c.out.stderr(), "SERVER ADDRESS: https://%v\n", httpServer.Addr)
+		fmt.Fprintf(c.out.stderr(), "SERVER ADDRESS: %shttps://%v\n", udsPfx, httpServer.Addr)
 		c.showServerUIPrompt(ctx)
 
 		return errors.Wrap(httpServer.ServeTLS(listener, c.serverStartTLSCertFile, c.serverStartTLSKeyFile), "error starting TLS server")
@@ -125,7 +140,7 @@ func (c *commandServerStart) startServerWithOptionalTLSAndListener(ctx context.C
 			fmt.Fprintf(c.out.stderr(), "SERVER CERTIFICATE: %v\n", base64.StdEncoding.EncodeToString(b.Bytes()))
 		}
 
-		fmt.Fprintf(c.out.stderr(), "SERVER ADDRESS: https://%v\n", httpServer.Addr)
+		fmt.Fprintf(c.out.stderr(), "SERVER ADDRESS: %shttps://%v\n", udsPfx, httpServer.Addr)
 		c.showServerUIPrompt(ctx)
 
 		return errors.Wrap(httpServer.ServeTLS(listener, "", ""), "error starting TLS server")
@@ -135,7 +150,7 @@ func (c *commandServerStart) startServerWithOptionalTLSAndListener(ctx context.C
 			return errors.Errorf("TLS not configured. To start server without encryption pass --insecure")
 		}
 
-		fmt.Fprintf(c.out.stderr(), "SERVER ADDRESS: http://%v\n", httpServer.Addr)
+		fmt.Fprintf(c.out.stderr(), "SERVER ADDRESS: %shttp://%v\n", udsPfx, httpServer.Addr)
 		c.showServerUIPrompt(ctx)
 
 		return errors.Wrap(httpServer.Serve(listener), "error starting server")
diff --git a/internal/apiclient/apiclient.go b/internal/apiclient/apiclient.go
index c6a9635c..5ca358ac 100644
--- a/internal/apiclient/apiclient.go
+++ b/internal/apiclient/apiclient.go
@@ -6,8 +6,10 @@
 	"context"
 	"encoding/json"
 	"io"
+	"net"
 	"net/http"
 	"net/http/cookiejar"
+	net_url "net/url"
 	"regexp"
 	"strings"
 
@@ -192,6 +194,20 @@ func NewKopiaAPIClient(options Options) (*KopiaAPIClient, error) {
 		transport = http.DefaultTransport
 	}
 
+	uri := options.BaseURL
+
+	if strings.HasPrefix(options.BaseURL, "unix+https://") || strings.HasPrefix(options.BaseURL, "unix+http://") {
+		u, _ := net_url.Parse(strings.TrimPrefix(options.BaseURL, "unix+"))
+		uri = u.Scheme + "://localhost"
+		tp, _ := transport.(*http.Transport)
+		transport = tp.Clone()
+		tp, _ = transport.(*http.Transport)
+		tp.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) {
+			dial, err := net.Dial("unix", u.Path)
+			return dial, errors.Wrap(err, "Failed to conect to socket: "+options.BaseURL)
+		}
+	}
+
 	// wrap with a round-tripper that provides basic authentication
 	if options.Username != "" || options.Password != "" {
 		transport = basicAuthTransport{transport, options.Username, options.Password}
@@ -207,7 +223,7 @@ func NewKopiaAPIClient(options Options) (*KopiaAPIClient, error) {
 	}
 
 	return &KopiaAPIClient{
-		options.BaseURL,
+		uri,
 		&http.Client{
 			Jar:       cj,
 			Transport: transport,
diff --git a/internal/gather/gather_write_buffer.go b/internal/gather/gather_write_buffer.go
index bc80bb41..6e1f0b4e 100644
--- a/internal/gather/gather_write_buffer.go
+++ b/internal/gather/gather_write_buffer.go
@@ -174,3 +174,8 @@ func (b *WriteBuffer) Dup() *WriteBuffer {
 func NewWriteBuffer() *WriteBuffer {
 	return &WriteBuffer{}
 }
+
+// NewWriteBufferMaxContiguous creates new write buffer that will allocate large chunks.
+func NewWriteBufferMaxContiguous() *WriteBuffer {
+	return &WriteBuffer{alloc: maxContiguousAllocator}
+}
diff --git a/repo/blob/sftp/sftp_storage.go b/repo/blob/sftp/sftp_storage.go
index bb310f2c..6b7fb28a 100644
--- a/repo/blob/sftp/sftp_storage.go
+++ b/repo/blob/sftp/sftp_storage.go
@@ -19,6 +19,7 @@
 
 	"github.com/kopia/kopia/internal/connection"
 	"github.com/kopia/kopia/internal/dirutil"
+	"github.com/kopia/kopia/internal/gather"
 	"github.com/kopia/kopia/internal/iocopy"
 	"github.com/kopia/kopia/internal/ospath"
 	"github.com/kopia/kopia/repo/blob"
@@ -194,6 +195,16 @@ func (s *sftpImpl) PutBlobInPath(ctx context.Context, dirPath, fullPath string,
 		return errors.Wrap(blob.ErrUnsupportedPutBlobOption, "do-not-recreate")
 	}
 
+	// SFTP client Write() does not do any buffering leading to sub-optimal
+	// performance of gather writes, so we copy the data to a contiguous
+	// temporary buffer first.
+	contig := gather.NewWriteBufferMaxContiguous()
+	defer contig.Close()
+
+	if _, err := data.WriteTo(contig); err != nil {
+		return errors.Wrap(err, "can't write to comtiguous buffer")
+	}
+
 	//nolint:wrapcheck
 	return s.rec.UsingConnectionNoResult(ctx, "PutBlobInPath", func(conn connection.Connection) error {
 		randSuffix := make([]byte, tempFileRandomSuffixLen)
@@ -208,7 +219,7 @@ func (s *sftpImpl) PutBlobInPath(ctx context.Context, dirPath, fullPath string,
 			return errors.Wrap(err, "cannot create temporary file")
 		}
 
-		if _, err = data.WriteTo(f); err != nil {
+		if _, err = contig.Bytes().WriteTo(f); err != nil {
 			return errors.Wrap(err, "can't write temporary file")
 		}
 
diff --git a/repo/grpc_repository_client.go b/repo/grpc_repository_client.go
index a1d690ef..809128da 100644
--- a/repo/grpc_repository_client.go
+++ b/repo/grpc_repository_client.go
@@ -806,12 +806,17 @@ func openGRPCAPIRepository(ctx context.Context, si *APIServerInfo, password stri
 		return nil, errors.Wrap(err, "unable to parse server URL")
 	}
 
-	if u.Scheme != "kopia" && u.Scheme != "https" {
-		return nil, errors.Errorf("invalid server address, must be 'https://host:port'")
+	if u.Scheme != "kopia" && u.Scheme != "https" && u.Scheme != "unix+https" {
+		return nil, errors.Errorf("invalid server address, must be 'https://host:port' or 'unix+https://<path>")
+	}
+
+	uri := u.Hostname() + ":" + u.Port()
+	if u.Scheme == "unix+https" {
+		uri = "unix:" + u.Path
 	}
 
 	conn, err := grpc.Dial(
-		u.Hostname()+":"+u.Port(),
+		uri,
 		grpc.WithPerRPCCredentials(grpcCreds{par.cliOpts.Hostname, par.cliOpts.Username, password}),
 		grpc.WithTransportCredentials(transportCreds),
 		grpc.WithDefaultCallOptions(
diff --git a/site/content/docs/Features/_index.md b/site/content/docs/Features/_index.md
index 6bfbb036..8427e79f 100644
--- a/site/content/docs/Features/_index.md
+++ b/site/content/docs/Features/_index.md
@@ -13,7 +13,7 @@ weight: 10
 * [Error Correction](#error-correction)
 * [Verifying Backup Validity and Consistency](#verifying-backup-validity-and-consistency)
 * [Recovering Backed Up Data When There is Data Loss](#recovering-backed-up-data-when-there-is-data-loss)
-* [Regular Automatic Maintence of Repositories](#regular-automatic-maintence-of-repositories)
+* [Regular Automatic Maintenance of Repositories](#regular-automatic-maintenance-of-repositories)
 * [Caching](#caching)
 * [Both Command Line and Graphical User Interfaces](#both-command-line-and-graphical-user-interfaces)
 * [Optional Server Mode with API Support to Centrally Manage Backups of Multiple Machines](#optional-server-mode-with-api-support-to-centrally-manage-backups-of-multiple-machines)
@@ -112,9 +112,9 @@ Backing up data is great, but you also need to be able to restore that data when
 
 Although never guaranteed, Kopia can often recover your files even if there is some partial data loss at your repository (e.g., a hard drive failure), because key index information and repository metadata is stored redundantly to prevent single points of failure. Note that Kopia cannot recover data where the actual backed up data file in the repository is corrupt, so make sure to regularly run repository consistency checks (see above discussion)!
 
-### Regular Automatic Maintence of Repositories
+### Regular Automatic Maintenance of Repositories
 
-Over time, repositories can get bloated to the point of decreased performance and waste of storage space. Kopia runs automatic maintence that ensures optimal performance and space usage. Read the [maintenance](../advanced/maintenance/) help docs for more information.
+Over time, repositories can get bloated to the point of decreased performance and waste of storage space. Kopia runs automatic maintenance that ensures optimal performance and space usage. Read the [maintenance](../advanced/maintenance/) help docs for more information.
 
 ### Caching
 
diff --git a/site/content/docs/Repository Server/_index.md b/site/content/docs/Repository Server/_index.md
index 365ece17..6612a42d 100644
--- a/site/content/docs/Repository Server/_index.md
+++ b/site/content/docs/Repository Server/_index.md
@@ -296,3 +296,29 @@ kopia server start --address 0.0.0.0:51515 --tls-cert-file ~/my.cert --tls-key-f
 ```
 
 You can now connect to your kopia server via reverse proxy with your domain: `mydomain.com:443`.
+
+Alternatively here is an example nginx/kopia configuration using unix domain sockets:
+
+```shell
+
+upstream socket {
+    server unix:///tmp/kopia.sock;
+}
+server {
+  listen 443 ssl http2;
+  server_name mydomain.com;
+
+  ssl_certificate_key /path/to/your/key.key;
+  ssl_certificate /path/to/your/cert.crt;
+
+  client_max_body_size 0;  # Allow unlimited upload size
+
+  location / {
+   grpc_pass grpcs://socket; # Adapt if your kopia is running on another server
+  }
+}
+```
+
+```shell
+kopia server start --address unix:/tmp/kopia.sock --tls-cert-file ~/my.cert --tls-key-file ~/my.key
+```
diff --git a/tools/gettool/checksums.txt b/tools/gettool/checksums.txt
index 8e3a1716..b153169b 100644
--- a/tools/gettool/checksums.txt
+++ b/tools/gettool/checksums.txt
@@ -7,12 +7,12 @@ https://github.com/git-chglog/git-chglog/releases/download/v0.15.1/git-chglog_0.
 https://github.com/gohugoio/hugo/releases/download/v0.113.0/hugo_extended_0.113.0_darwin-universal.tar.gz: 1557f896f34743d241e1aecab588be273dde59692b362a9f4488231a2595b2ae
 https://github.com/gohugoio/hugo/releases/download/v0.113.0/hugo_extended_0.113.0_linux-amd64.tar.gz: e04bccfa81df6c727f1c03bc858eb21d6f95123d311cafe245f4485d289123f3
 https://github.com/gohugoio/hugo/releases/download/v0.113.0/hugo_extended_0.113.0_windows-amd64.zip: 3eabfbfad1431939058e6f7e76573c6bac1fee92f3a7b1ac5739c555940f0e0e
-https://github.com/golangci/golangci-lint/releases/download/v1.53.3/golangci-lint-1.53.3-darwin-amd64.tar.gz: e6fe5df023c35482cf9858eeb0a14aeecea58e64549be9084268b4a1fb632ece
-https://github.com/golangci/golangci-lint/releases/download/v1.53.3/golangci-lint-1.53.3-darwin-arm64.tar.gz: 76607909a15e825a39bd61f1c5805997746b365bd285314277dccde1b86edae6
-https://github.com/golangci/golangci-lint/releases/download/v1.53.3/golangci-lint-1.53.3-linux-amd64.tar.gz: 4f62007ca96372ccba54760e2ed39c2446b40ec24d9a90c21aad9f2fdf6cf0da
-https://github.com/golangci/golangci-lint/releases/download/v1.53.3/golangci-lint-1.53.3-linux-arm64.tar.gz: 2bec2e7c0eb20d9a7859d9a2a534ec44e3782946b4e4928546c1fd54b8644bc4
-https://github.com/golangci/golangci-lint/releases/download/v1.53.3/golangci-lint-1.53.3-linux-armv6.tar.gz: 1dda9778ca1b9d521033f57db7bf07a1958f24f648fcc8715e071c5ec62c5326
-https://github.com/golangci/golangci-lint/releases/download/v1.53.3/golangci-lint-1.53.3-windows-amd64.zip: bd23cc509f00990eecebeb2f1a12ba1b07f395c53313a27da969fad99b686ceb
+https://github.com/golangci/golangci-lint/releases/download/v1.54.0/golangci-lint-1.54.0-darwin-amd64.tar.gz: 0a76fcb91bca94c0b3bcb931662eafd320fbe458b3a29ce368b0bffbd4eff2fb
+https://github.com/golangci/golangci-lint/releases/download/v1.54.0/golangci-lint-1.54.0-darwin-arm64.tar.gz: aeb77a00c24720e223ef73da18eea3afb29ea46356db33e1f503c66f2799d387
+https://github.com/golangci/golangci-lint/releases/download/v1.54.0/golangci-lint-1.54.0-linux-amd64.tar.gz: a694f19dbfab3ea4d3956cb105d2e74c1dc49cb4c06ece903a3c534bce86b3dc
+https://github.com/golangci/golangci-lint/releases/download/v1.54.0/golangci-lint-1.54.0-linux-arm64.tar.gz: c25165722b7c12ba7d72a15c3dd838e556ba23f24b2857309cbae9c4067d47a7
+https://github.com/golangci/golangci-lint/releases/download/v1.54.0/golangci-lint-1.54.0-linux-armv6.tar.gz: cf789c827da074fc0418b73f6605f44c6e3715d530f9015f75690a9e6441112e
+https://github.com/golangci/golangci-lint/releases/download/v1.54.0/golangci-lint-1.54.0-windows-amd64.zip: 8ff567bfe2add55764b983826ca83a9ef9cf063075f36f4818ddc1c73ed62e6d
 https://github.com/goreleaser/goreleaser/releases/download/v0.176.0/goreleaser_Darwin_arm64.tar.gz: 1f95e6561974f4766d8833438b646b06930563ca9867447ea03edb623d876c75
 https://github.com/goreleaser/goreleaser/releases/download/v0.176.0/goreleaser_Darwin_x86_64.tar.gz: 17ecad881a50e32f033da5a200c8417d37cae70f09e925645452937998aca506
 https://github.com/goreleaser/goreleaser/releases/download/v0.176.0/goreleaser_Linux_arm64.tar.gz: 8bf2a9b9e84498bfa239f2fe91b2d555642c87ab9d3f5d37f29e6e97116910a3
diff --git a/tools/tools.mk b/tools/tools.mk
index 225809b3..c2eb8507 100644
--- a/tools/tools.mk
+++ b/tools/tools.mk
@@ -102,7 +102,7 @@ retry:=
 endif
 
 # tool versions
-GOLANGCI_LINT_VERSION=1.53.3
+GOLANGCI_LINT_VERSION=1.54.0
 CHECKLOCKS_VERSION=release-20221026.0
 NODE_VERSION=18.16.0
 HUGO_VERSION=0.113.0


Test patch:
diff --git a/cli/command_server_control_test.go b/cli/command_server_control_test.go
index 161d9fe2..a9b1307f 100644
--- a/cli/command_server_control_test.go
+++ b/cli/command_server_control_test.go
@@ -1,6 +1,7 @@
 package cli_test
 
 import (
+	"runtime"
 	"testing"
 	"time"
 
@@ -162,6 +163,61 @@ func TestServerControl(t *testing.T) {
 	env.RunAndExpectFailure(t, "server", "shutdown", "--address", sp.BaseURL, "--server-control-password", sp.ServerControlPassword)
 }
 
+func TestServerControlUDS(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip()
+	}
+
+	env := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, testenv.NewInProcRunner(t))
+
+	dir0 := testutil.TempDirectory(t)
+	// the socket path must be < 108 bytes (linux) or 104 bytes (Mac), so can't use long tempdir
+	dir1 := testutil.TempDirectoryShort(t)
+
+	env.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", env.RepoDir, "--override-username=another-user", "--override-hostname=another-host")
+	env.RunAndExpectSuccess(t, "snap", "create", dir0)
+
+	env.RunAndExpectSuccess(t, "repo", "connect", "filesystem", "--path", env.RepoDir, "--override-username=test-user", "--override-hostname=test-host")
+
+	serverStarted := make(chan struct{})
+	serverStopped := make(chan struct{})
+
+	var sp testutil.ServerParameters
+
+	go func() {
+		wait, _ := env.RunAndProcessStderr(t, sp.ProcessOutput,
+			"server", "start", "--insecure", "--random-server-control-password", "--address="+"unix:"+dir1+"/sock")
+
+		close(serverStarted)
+
+		wait()
+
+		close(serverStopped)
+	}()
+
+	select {
+	case <-serverStarted:
+		t.Logf("server started on %v", sp.BaseURL)
+
+	case <-time.After(5 * time.Second):
+		t.Fatalf("server did not start in time")
+	}
+
+	lines := env.RunAndExpectSuccess(t, "server", "status", "--address", sp.BaseURL, "--server-control-password", sp.ServerControlPassword, "--remote")
+	require.Len(t, lines, 1)
+	require.Contains(t, lines, "REMOTE: another-user@another-host:"+dir0)
+
+	env.RunAndExpectSuccess(t, "server", "shutdown", "--address", sp.BaseURL, "--server-control-password", sp.ServerControlPassword)
+
+	select {
+	case <-serverStopped:
+		t.Logf("server shut down")
+
+	case <-time.After(15 * time.Second):
+		t.Fatalf("server did not shutdown in time")
+	}
+}
+
 func hasLine(lines []string, lookFor string) bool {
 	for _, l := range lines {
 		if l == lookFor {
diff --git a/internal/gather/gather_write_buffer_test.go b/internal/gather/gather_write_buffer_test.go
index 0af48e6b..5a99aa71 100644
--- a/internal/gather/gather_write_buffer_test.go
+++ b/internal/gather/gather_write_buffer_test.go
@@ -107,3 +107,24 @@ func TestGatherWriteBufferAllocatorSelector(t *testing.T) {
 	w.MakeContiguous(maxContiguousAllocator.chunkSize + 1)
 	require.Nil(t, w.alloc)
 }
+
+func TestGatherWriteBufferMax(t *testing.T) {
+	b := NewWriteBufferMaxContiguous()
+	defer b.Close()
+
+	// write 1Mx5 bytes
+	for i := 0; i < 1000000; i++ {
+		b.Append([]byte("hello"))
+	}
+
+	// make sure we have 1 contiguous buffer
+	require.Equal(t, 1, len(b.Bytes().Slices))
+
+	// write 10Mx5 bytes
+	for i := 0; i < 10000000; i++ {
+		b.Append([]byte("hello"))
+	}
+
+	// 51M requires 4x16MB buffers
+	require.Equal(t, 4, len(b.Bytes().Slices))
+}
diff --git a/internal/testutil/tmpdir.go b/internal/testutil/tmpdir.go
index d6d42f0b..24c26f1e 100644
--- a/internal/testutil/tmpdir.go
+++ b/internal/testutil/tmpdir.go
@@ -68,6 +68,27 @@ func TempDirectory(tb testing.TB) string {
 	return d
 }
 
+// TempDirectoryShort returns a short temporary directory and cleans it up before test
+// completes.
+func TempDirectoryShort(tb testing.TB) string {
+	tb.Helper()
+
+	d, err := os.MkdirTemp("", "kopia-test")
+	if err != nil {
+		tb.Fatal(errors.Wrap(err, "unable to create temp directory"))
+	}
+
+	tb.Cleanup(func() {
+		if !tb.Failed() {
+			os.RemoveAll(d) //nolint:errcheck
+		} else {
+			tb.Logf("temporary files left in %v", d)
+		}
+	})
+
+	return d
+}
+
 // TempLogDirectory returns a temporary directory used for storing logs.
 // If KOPIA_LOGS_DIR is provided.
 func TempLogDirectory(t *testing.T) string {