Repository: supabase/cli
Pull request #2589 (2024-08-06 02:17:38)

Prod deploy
- **fix: bump edge-runtime to 1.55.2 (#2579)**
- **fix: upgrade auth version to v2.157.1 and add slack oidc config (#2583)**
- **fix: use unix diff for clearer config changes**
- **chore: show password reset link on missing password**
- **chore(deps): bump golang.org/x/mod from 0.19.0 to 0.20.0 (#2585)**
- **fix: bump edge-runtime to 1.56.0 (#2580)**



Patch:
diff --git a/cmd/seed.go b/cmd/seed.go
index 760a9424..c36b7074 100644
--- a/cmd/seed.go
+++ b/cmd/seed.go
@@ -4,6 +4,7 @@ import (
 	"os"
 	"os/signal"
 
+	"github.com/spf13/afero"
 	"github.com/spf13/cobra"
 	"github.com/supabase/cli/internal/seed/buckets"
 	"github.com/supabase/cli/internal/utils"
@@ -27,7 +28,7 @@ var (
 		Short: "Seed buckets declared in [storage.buckets]",
 		Args:  cobra.NoArgs,
 		RunE: func(cmd *cobra.Command, args []string) error {
-			return buckets.Run(cmd.Context(), flags.ProjectRef, utils.NewConsole())
+			return buckets.Run(cmd.Context(), flags.ProjectRef, true, afero.NewOsFs())
 		},
 	}
 )
diff --git a/docs/main.go b/docs/main.go
index b791dedc..bfacba31 100644
--- a/docs/main.go
+++ b/docs/main.go
@@ -185,7 +185,7 @@ func GenYamlDoc(cmd *cobra.Command, root *SpecDoc) CmdDoc {
 		Subcommands: subcommands,
 	}
 
-	names := strings.Split(cmd.CommandPath(), " ")
+	names := strings.Fields(cmd.CommandPath())
 	if len(names) > 3 {
 		base := strings.Join(names[2:], "-")
 		names = append(names[:2], base)
diff --git a/go.mod b/go.mod
index df5e501f..fe4ede74 100644
--- a/go.mod
+++ b/go.mod
@@ -13,8 +13,8 @@ require (
 	github.com/charmbracelet/lipgloss v0.12.1
 	github.com/containers/common v0.59.2
 	github.com/deepmap/oapi-codegen/v2 v2.2.0
-	github.com/docker/cli v26.1.2+incompatible
-	github.com/docker/docker v26.1.4+incompatible
+	github.com/docker/cli v26.1.5+incompatible
+	github.com/docker/docker v26.1.5+incompatible
 	github.com/docker/go-connections v0.5.0
 	github.com/docker/go-units v0.5.0
 	github.com/getsentry/sentry-go v0.28.1
@@ -48,7 +48,8 @@ require (
 	github.com/stripe/pg-schema-diff v0.7.0
 	github.com/withfig/autocomplete-tools/packages/cobra v1.2.0
 	github.com/zalando/go-keyring v0.2.5
-	golang.org/x/mod v0.19.0
+	go.opentelemetry.io/otel v1.28.0
+	golang.org/x/mod v0.20.0
 	golang.org/x/oauth2 v0.21.0
 	golang.org/x/term v0.22.0
 	google.golang.org/grpc v1.65.0
@@ -317,7 +318,6 @@ require (
 	go-simpler.org/musttag v0.12.2 // indirect
 	go-simpler.org/sloglint v0.7.1 // indirect
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
-	go.opentelemetry.io/otel v1.28.0 // indirect
 	go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 // indirect
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // indirect
diff --git a/go.sum b/go.sum
index 9dd3bac9..9dcf2a2a 100644
--- a/go.sum
+++ b/go.sum
@@ -234,13 +234,13 @@ github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E
 github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
 github.com/dnephin/pflag v1.0.7 h1:oxONGlWxhmUct0YzKTgrpQv9AUA1wtPBn7zuSjJqptk=
 github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE=
-github.com/docker/cli v26.1.2+incompatible h1:/MWZpUMMlr1hCGyquL8QNbL1hbivQ1kLuT3Z9s1Tlpg=
-github.com/docker/cli v26.1.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/cli v26.1.5+incompatible h1:NxXGSdz2N+Ibdaw330TDO3d/6/f7MvHuiMbuFaIQDTk=
+github.com/docker/cli v26.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
 github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU=
-github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v26.1.5+incompatible h1:NEAxTwEjxV6VbBMBoGG3zPqbiJosIApZjxlbrG9q3/g=
+github.com/docker/docker v26.1.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo=
 github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
 github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
@@ -1146,8 +1146,8 @@ golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
-golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
+golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go
index 10d8667c..06b05e58 100644
--- a/internal/bootstrap/bootstrap.go
+++ b/internal/bootstrap/bootstrap.go
@@ -30,6 +30,7 @@ import (
 	"github.com/supabase/cli/internal/utils/tenant"
 	"github.com/supabase/cli/pkg/api"
 	"github.com/supabase/cli/pkg/fetcher"
+	"github.com/supabase/cli/pkg/queue"
 	"golang.org/x/term"
 )
 
@@ -362,14 +363,14 @@ func downloadSample(ctx context.Context, client *github.Client, templateUrl stri
 
 type Downloader struct {
 	api   *fetcher.Fetcher
-	queue *utils.JobQueue
+	queue *queue.JobQueue
 	fsys  afero.Fs
 }
 
 func NewDownloader(concurrency uint, fsys afero.Fs) *Downloader {
 	return &Downloader{
 		api:   fetcher.NewFetcher("", fetcher.WithExpectedStatus(http.StatusOK)),
-		queue: utils.NewJobQueue(concurrency),
+		queue: queue.NewJobQueue(concurrency),
 		fsys:  fsys,
 	}
 }
diff --git a/internal/db/reset/reset.go b/internal/db/reset/reset.go
index cb856899..be4802fb 100644
--- a/internal/db/reset/reset.go
+++ b/internal/db/reset/reset.go
@@ -8,6 +8,7 @@ import (
 	"os"
 	"strconv"
 	"strings"
+	"time"
 
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/network"
@@ -21,6 +22,7 @@ import (
 	"github.com/supabase/cli/internal/gen/keys"
 	"github.com/supabase/cli/internal/migration/apply"
 	"github.com/supabase/cli/internal/migration/repair"
+	"github.com/supabase/cli/internal/seed/buckets"
 	"github.com/supabase/cli/internal/utils"
 	"github.com/supabase/cli/pkg/migration"
 )
@@ -43,17 +45,21 @@ func Run(ctx context.Context, version string, config pgconn.Config, fsys afero.F
 		}
 		return resetRemote(ctx, version, config, fsys, options...)
 	}
-
 	// Config file is loaded before parsing --linked or --local flags
 	if err := utils.AssertSupabaseDbIsRunning(); err != nil {
 		return err
 	}
-
 	// Reset postgres database because extensions (pg_cron, pg_net) require postgres
 	if err := resetDatabase(ctx, version, fsys, options...); err != nil {
 		return err
 	}
-
+	// Seed objects from supabase/buckets directory
+	if err := start.WaitForHealthyService(ctx, 30*time.Second, utils.StorageId); err != nil {
+		return err
+	}
+	if err := buckets.Run(ctx, "", false, fsys); err != nil {
+		return err
+	}
 	branch := keys.GetGitBranch(fsys)
 	fmt.Fprintln(os.Stderr, "Finished "+utils.Aqua("supabase db reset")+" on branch "+utils.Aqua(branch)+".")
 	return nil
@@ -199,7 +205,7 @@ func RestartDatabase(ctx context.Context, w io.Writer) error {
 
 func restartServices(ctx context.Context) error {
 	// No need to restart PostgREST because it automatically reconnects and listens for schema changes
-	services := []string{utils.StorageId, utils.GotrueId, utils.RealtimeId}
+	services := listServicesToRestart()
 	result := utils.WaitAll(services, func(id string) error {
 		if err := utils.Docker.ContainerRestart(ctx, id, container.StopOptions{}); err != nil && !errdefs.IsNotFound(err) {
 			return errors.Errorf("Failed to restart %s: %w", id, err)
@@ -210,6 +216,10 @@ func restartServices(ctx context.Context) error {
 	return errors.Join(result...)
 }
 
+func listServicesToRestart() []string {
+	return []string{utils.StorageId, utils.GotrueId, utils.RealtimeId, utils.PoolerId}
+}
+
 func resetRemote(ctx context.Context, version string, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
 	fmt.Fprintln(os.Stderr, "Resetting remote database"+toLogMessage(version))
 	conn, err := utils.ConnectByConfigStream(ctx, config, io.Discard, options...)
diff --git a/internal/link/diff.go b/internal/link/diff.go
new file mode 100644
index 00000000..84f2e349
--- /dev/null
+++ b/internal/link/diff.go
@@ -0,0 +1,261 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package link
+
+import (
+	"bytes"
+	"fmt"
+	"sort"
+	"strings"
+)
+
+// A pair is a pair of values tracked for both the x and y side of a diff.
+// It is typically a pair of line indexes.
+type pair struct{ x, y int }
+
+// Diff returns an anchored diff of the two texts old and new
+// in the “unified diff” format. If old and new are identical,
+// Diff returns a nil slice (no output).
+//
+// Unix diff implementations typically look for a diff with
+// the smallest number of lines inserted and removed,
+// which can in the worst case take time quadratic in the
+// number of lines in the texts. As a result, many implementations
+// either can be made to run for a long time or cut off the search
+// after a predetermined amount of work.
+//
+// In contrast, this implementation looks for a diff with the
+// smallest number of “unique” lines inserted and removed,
+// where unique means a line that appears just once in both old and new.
+// We call this an “anchored diff” because the unique lines anchor
+// the chosen matching regions. An anchored diff is usually clearer
+// than a standard diff, because the algorithm does not try to
+// reuse unrelated blank lines or closing braces.
+// The algorithm also guarantees to run in O(n log n) time
+// instead of the standard O(n²) time.
+//
+// Some systems call this approach a “patience diff,” named for
+// the “patience sorting” algorithm, itself named for a solitaire card game.
+// We avoid that name for two reasons. First, the name has been used
+// for a few different variants of the algorithm, so it is imprecise.
+// Second, the name is frequently interpreted as meaning that you have
+// to wait longer (to be patient) for the diff, meaning that it is a slower algorithm,
+// when in fact the algorithm is faster than the standard one.
+func Diff(oldName string, old []byte, newName string, new []byte) []byte {
+	if bytes.Equal(old, new) {
+		return nil
+	}
+	x := lines(old)
+	y := lines(new)
+
+	// Print diff header.
+	var out bytes.Buffer
+	fmt.Fprintf(&out, "diff %s %s\n", oldName, newName)
+	fmt.Fprintf(&out, "--- %s\n", oldName)
+	fmt.Fprintf(&out, "+++ %s\n", newName)
+
+	// Loop over matches to consider,
+	// expanding each match to include surrounding lines,
+	// and then printing diff chunks.
+	// To avoid setup/teardown cases outside the loop,
+	// tgs returns a leading {0,0} and trailing {len(x), len(y)} pair
+	// in the sequence of matches.
+	var (
+		done  pair     // printed up to x[:done.x] and y[:done.y]
+		chunk pair     // start lines of current chunk
+		count pair     // number of lines from each side in current chunk
+		ctext []string // lines for current chunk
+	)
+	for _, m := range tgs(x, y) {
+		if m.x < done.x {
+			// Already handled scanning forward from earlier match.
+			continue
+		}
+
+		// Expand matching lines as far as possible,
+		// establishing that x[start.x:end.x] == y[start.y:end.y].
+		// Note that on the first (or last) iteration we may (or definitely do)
+		// have an empty match: start.x==end.x and start.y==end.y.
+		start := m
+		for start.x > done.x && start.y > done.y && x[start.x-1] == y[start.y-1] {
+			start.x--
+			start.y--
+		}
+		end := m
+		for end.x < len(x) && end.y < len(y) && x[end.x] == y[end.y] {
+			end.x++
+			end.y++
+		}
+
+		// Emit the mismatched lines before start into this chunk.
+		// (No effect on first sentinel iteration, when start = {0,0}.)
+		for _, s := range x[done.x:start.x] {
+			ctext = append(ctext, "-"+s)
+			count.x++
+		}
+		for _, s := range y[done.y:start.y] {
+			ctext = append(ctext, "+"+s)
+			count.y++
+		}
+
+		// If we're not at EOF and have too few common lines,
+		// the chunk includes all the common lines and continues.
+		const C = 3 // number of context lines
+		if (end.x < len(x) || end.y < len(y)) &&
+			(end.x-start.x < C || (len(ctext) > 0 && end.x-start.x < 2*C)) {
+			for _, s := range x[start.x:end.x] {
+				ctext = append(ctext, " "+s)
+				count.x++
+				count.y++
+			}
+			done = end
+			continue
+		}
+
+		// End chunk with common lines for context.
+		if len(ctext) > 0 {
+			n := end.x - start.x
+			if n > C {
+				n = C
+			}
+			for _, s := range x[start.x : start.x+n] {
+				ctext = append(ctext, " "+s)
+				count.x++
+				count.y++
+			}
+			done = pair{start.x + n, start.y + n}
+
+			// Format and emit chunk.
+			// Convert line numbers to 1-indexed.
+			// Special case: empty file shows up as 0,0 not 1,0.
+			if count.x > 0 {
+				chunk.x++
+			}
+			if count.y > 0 {
+				chunk.y++
+			}
+			fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", chunk.x, count.x, chunk.y, count.y)
+			for _, s := range ctext {
+				out.WriteString(s)
+			}
+			count.x = 0
+			count.y = 0
+			ctext = ctext[:0]
+		}
+
+		// If we reached EOF, we're done.
+		if end.x >= len(x) && end.y >= len(y) {
+			break
+		}
+
+		// Otherwise start a new chunk.
+		chunk = pair{end.x - C, end.y - C}
+		for _, s := range x[chunk.x:end.x] {
+			ctext = append(ctext, " "+s)
+			count.x++
+			count.y++
+		}
+		done = end
+	}
+
+	return out.Bytes()
+}
+
+// lines returns the lines in the file x, including newlines.
+// If the file does not end in a newline, one is supplied
+// along with a warning about the missing newline.
+func lines(x []byte) []string {
+	l := strings.SplitAfter(string(x), "\n")
+	if l[len(l)-1] == "" {
+		l = l[:len(l)-1]
+	} else {
+		// Treat last line as having a message about the missing newline attached,
+		// using the same text as BSD/GNU diff (including the leading backslash).
+		l[len(l)-1] += "\n\\ No newline at end of file\n"
+	}
+	return l
+}
+
+// tgs returns the pairs of indexes of the longest common subsequence
+// of unique lines in x and y, where a unique line is one that appears
+// once in x and once in y.
+//
+// The longest common subsequence algorithm is as described in
+// Thomas G. Szymanski, “A Special Case of the Maximal Common
+// Subsequence Problem,” Princeton TR #170 (January 1975),
+// available at https://research.swtch.com/tgs170.pdf.
+func tgs(x, y []string) []pair {
+	// Count the number of times each string appears in a and b.
+	// We only care about 0, 1, many, counted as 0, -1, -2
+	// for the x side and 0, -4, -8 for the y side.
+	// Using negative numbers now lets us distinguish positive line numbers later.
+	m := make(map[string]int)
+	for _, s := range x {
+		if c := m[s]; c > -2 {
+			m[s] = c - 1
+		}
+	}
+	for _, s := range y {
+		if c := m[s]; c > -8 {
+			m[s] = c - 4
+		}
+	}
+
+	// Now unique strings can be identified by m[s] = -1+-4.
+	//
+	// Gather the indexes of those strings in x and y, building:
+	//	xi[i] = increasing indexes of unique strings in x.
+	//	yi[i] = increasing indexes of unique strings in y.
+	//	inv[i] = index j such that x[xi[i]] = y[yi[j]].
+	var xi, yi, inv []int
+	for i, s := range y {
+		if m[s] == -1+-4 {
+			m[s] = len(yi)
+			yi = append(yi, i)
+		}
+	}
+	for i, s := range x {
+		if j, ok := m[s]; ok && j >= 0 {
+			xi = append(xi, i)
+			inv = append(inv, j)
+		}
+	}
+
+	// Apply Algorithm A from Szymanski's paper.
+	// In those terms, A = J = inv and B = [0, n).
+	// We add sentinel pairs {0,0}, and {len(x),len(y)}
+	// to the returned sequence, to help the processing loop.
+	J := inv
+	n := len(xi)
+	T := make([]int, n)
+	L := make([]int, n)
+	for i := range T {
+		T[i] = n + 1
+	}
+	for i := 0; i < n; i++ {
+		k := sort.Search(n, func(k int) bool {
+			return T[k] >= J[i]
+		})
+		T[k] = J[i]
+		L[i] = k + 1
+	}
+	k := 0
+	for _, v := range L {
+		if k < v {
+			k = v
+		}
+	}
+	seq := make([]pair, 2+k)
+	seq[1+k] = pair{len(x), len(y)} // sentinel at end
+	lastj := n
+	for i := n - 1; i >= 0; i-- {
+		if L[i] == k && J[i] < lastj {
+			seq[k] = pair{xi[i], yi[J[i]]}
+			k--
+		}
+	}
+	seq[0] = pair{0, 0} // sentinel at start
+	return seq
+}
diff --git a/internal/link/link.go b/internal/link/link.go
index 172ea09f..9113f543 100644
--- a/internal/link/link.go
+++ b/internal/link/link.go
@@ -11,7 +11,6 @@ import (
 
 	"github.com/BurntSushi/toml"
 	"github.com/go-errors/errors"
-	"github.com/google/go-cmp/cmp"
 	"github.com/jackc/pgconn"
 	"github.com/jackc/pgx/v4"
 	"github.com/spf13/afero"
@@ -26,7 +25,7 @@ import (
 )
 
 func Run(ctx context.Context, projectRef string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
-	original := toTomlLines(map[string]interface{}{
+	original := toTomlBytes(map[string]interface{}{
 		"api": utils.Config.Api,
 		"db":  utils.Config.Db,
 	})
@@ -56,25 +55,26 @@ func Run(ctx context.Context, projectRef string, fsys afero.Fs, options ...func(
 	fmt.Fprintln(os.Stdout, "Finished "+utils.Aqua("supabase link")+".")
 
 	// 4. Suggest config update
-	updated := toTomlLines(map[string]interface{}{
+	updated := toTomlBytes(map[string]interface{}{
 		"api": utils.Config.Api,
 		"db":  utils.Config.Db,
 	})
-	if lineDiff := cmp.Diff(original, updated); len(lineDiff) > 0 {
+	// if lineDiff := cmp.Diff(original, updated); len(lineDiff) > 0 {
+	if lineDiff := Diff(utils.ConfigPath, original, projectRef, updated); len(lineDiff) > 0 {
 		fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "Local config differs from linked project. Try updating", utils.Bold(utils.ConfigPath))
-		fmt.Println(lineDiff)
+		fmt.Println(string(lineDiff))
 	}
 	return nil
 }
 
-func toTomlLines(config any) []string {
+func toTomlBytes(config any) []byte {
 	var buf bytes.Buffer
 	enc := toml.NewEncoder(&buf)
 	enc.Indent = ""
 	if err := enc.Encode(config); err != nil {
 		fmt.Fprintln(utils.GetDebugLogger(), "failed to marshal toml config:", err)
 	}
-	return strings.Split(buf.String(), "\n")
+	return buf.Bytes()
 }
 
 func LinkServices(ctx context.Context, projectRef, anonKey string, fsys afero.Fs) {
diff --git a/internal/seed/buckets/buckets.go b/internal/seed/buckets/buckets.go
index 2801405a..2af80b43 100644
--- a/internal/seed/buckets/buckets.go
+++ b/internal/seed/buckets/buckets.go
@@ -3,18 +3,22 @@ package buckets
 import (
 	"context"
 	"fmt"
+	"path/filepath"
 
+	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/storage/client"
 	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/pkg/config"
 )
 
-func Run(ctx context.Context, projectRef string, console *utils.Console) error {
+func Run(ctx context.Context, projectRef string, interactive bool, fsys afero.Fs) error {
 	api, err := client.NewStorageAPI(ctx, projectRef)
 	if err != nil {
 		return err
 	}
-	if console == nil {
-		return api.UpsertBuckets(ctx, utils.Config.Storage.Buckets)
+	console := utils.NewConsole()
+	if !interactive {
+		console.IsTTY = false
 	}
 	filter := func(bucketId string) bool {
 		label := fmt.Sprintf("Bucket %s already exists. Do you want to overwrite its properties?", utils.Bold(bucketId))
@@ -24,5 +28,15 @@ func Run(ctx context.Context, projectRef string, console *utils.Console) error {
 		}
 		return shouldOverwrite
 	}
-	return api.UpsertBuckets(ctx, utils.Config.Storage.Buckets, filter)
+	if err := api.UpsertBuckets(ctx, utils.Config.Storage.Buckets, filter); err != nil {
+		return err
+	}
+	resolved := config.BucketConfig{}
+	for name, bucket := range utils.Config.Storage.Buckets {
+		if len(bucket.ObjectsPath) > 0 && !filepath.IsAbs(bucket.ObjectsPath) {
+			bucket.ObjectsPath = filepath.Join(utils.SupabaseDirPath, bucket.ObjectsPath)
+		}
+		resolved[name] = bucket
+	}
+	return api.UpsertObjects(ctx, resolved, utils.NewRootFS(fsys))
 }
diff --git a/internal/start/start.go b/internal/start/start.go
index ade4cc98..64c5e244 100644
--- a/internal/start/start.go
+++ b/internal/start/start.go
@@ -105,6 +105,7 @@ type kongConfig struct {
 	PgmetaId      string
 	EdgeRuntimeId string
 	LogflareId    string
+	PoolerId      string
 	ApiHost       string
 	ApiPort       uint16
 }
@@ -350,6 +351,7 @@ EOF
 			PgmetaId:      utils.PgmetaId,
 			EdgeRuntimeId: utils.EdgeRuntimeId,
 			LogflareId:    utils.LogflareId,
+			PoolerId:      utils.PoolerId,
 			ApiHost:       utils.Config.Hostname,
 			ApiPort:       utils.Config.Api.Port,
 		}); err != nil {
@@ -1062,7 +1064,7 @@ EOF
 			return err
 		}
 		// Disable prompts when seeding
-		if err := buckets.Run(ctx, "", nil); err != nil {
+		if err := buckets.Run(ctx, "", false, fsys); err != nil {
 			return err
 		}
 	}
diff --git a/internal/start/templates/kong.yml b/internal/start/templates/kong.yml
index d1a81676..6927edde 100644
--- a/internal/start/templates/kong.yml
+++ b/internal/start/templates/kong.yml
@@ -140,3 +140,14 @@ services:
         strip_path: true
         paths:
           - /analytics/v1/
+  - name: pooler-v2-ws
+    _comment: "Pooler: /pooler/v2/* -> ws://pooler:4000/v2/*"
+    url: http://{{ .PoolerId }}:4000/v2
+    protocol: ws
+    routes:
+      - name: pooler-v2-ws
+        strip_path: true
+        paths:
+          - /pooler/v2/
+    plugins:
+      - name: cors
diff --git a/internal/storage/cp/cp.go b/internal/storage/cp/cp.go
index 9719a726..3676e034 100644
--- a/internal/storage/cp/cp.go
+++ b/internal/storage/cp/cp.go
@@ -16,6 +16,7 @@ import (
 	"github.com/supabase/cli/internal/storage/ls"
 	"github.com/supabase/cli/internal/utils"
 	"github.com/supabase/cli/internal/utils/flags"
+	"github.com/supabase/cli/pkg/queue"
 	"github.com/supabase/cli/pkg/storage"
 )
 
@@ -66,7 +67,7 @@ func DownloadStorageObjectAll(ctx context.Context, api storage.StorageAPI, remot
 	}
 	// No need to be atomic because it's incremented only on main thread
 	count := 0
-	jq := utils.NewJobQueue(maxJobs)
+	jq := queue.NewJobQueue(maxJobs)
 	err := ls.IterateStoragePathsAll(ctx, api, remotePath, func(objectPath string) error {
 		relPath := strings.TrimPrefix(objectPath, remotePath)
 		dstPath := filepath.Join(localPath, filepath.FromSlash(relPath))
@@ -79,7 +80,13 @@ func DownloadStorageObjectAll(ctx context.Context, api storage.StorageAPI, remot
 			if err := utils.MkdirIfNotExistFS(fsys, filepath.Dir(dstPath)); err != nil {
 				return err
 			}
-			return api.DownloadObject(ctx, objectPath, dstPath, fsys)
+			// Overwrites existing file when using --recursive flag
+			f, err := fsys.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+			if err != nil {
+				return errors.Errorf("failed to create file: %w", err)
+			}
+			defer f.Close()
+			return api.DownloadObjectStream(ctx, objectPath, f)
 		}
 		return jq.Put(job)
 	})
@@ -94,19 +101,26 @@ func UploadStorageObjectAll(ctx context.Context, api storage.StorageAPI, remoteP
 	// Check if directory exists on remote
 	dirExists := false
 	fileExists := false
-	if err := ls.IterateStoragePaths(ctx, api, noSlash, func(objectName string) error {
-		if objectName == path.Base(noSlash) {
-			fileExists = true
+	if len(noSlash) > 0 {
+		callback := func(objectName string) error {
+			if objectName == path.Base(noSlash) {
+				fileExists = true
+			}
+			if objectName == path.Base(noSlash)+"/" {
+				dirExists = true
+			}
+			return nil
 		}
-		if objectName == path.Base(noSlash)+"/" {
-			dirExists = true
+		if err := ls.IterateStoragePaths(ctx, api, noSlash, callback); err != nil {
+			return err
 		}
-		return nil
-	}); err != nil {
-		return err
 	}
+	// Overwrites existing object when using --recursive flag
+	opts = append(opts, func(fo *storage.FileOptions) {
+		fo.Overwrite = true
+	})
 	baseName := filepath.Base(localPath)
-	jq := utils.NewJobQueue(maxJobs)
+	jq := queue.NewJobQueue(maxJobs)
 	err := afero.Walk(fsys, localPath, func(filePath string, info fs.FileInfo, err error) error {
 		if err != nil {
 			return errors.New(err)
@@ -123,6 +137,7 @@ func UploadStorageObjectAll(ctx context.Context, api storage.StorageAPI, remoteP
 		if relPath == "." {
 			_, prefix := client.SplitBucketPrefix(dstPath)
 			if IsDir(prefix) || (dirExists && !fileExists) {
+				// Keep the file name when destination is a directory
 				dstPath = path.Join(dstPath, info.Name())
 			}
 		} else {
diff --git a/internal/utils/api.go b/internal/utils/api.go
index 90e6a261..3dc63d30 100644
--- a/internal/utils/api.go
+++ b/internal/utils/api.go
@@ -264,7 +264,7 @@ func GetSupabaseDashboardURL() string {
 	case DefaultApiHost, DeprecatedApiHost:
 		return "https://supabase.com/dashboard"
 	case "https://api.supabase.green":
-		return "https://app.supabase.green"
+		return "https://supabase.green/dashboard"
 	default:
 		return "http://127.0.0.1:8082"
 	}
diff --git a/internal/utils/config.go b/internal/utils/config.go
index e7fb6dd5..3f89dc75 100644
--- a/internal/utils/config.go
+++ b/internal/utils/config.go
@@ -3,6 +3,7 @@ package utils
 import (
 	_ "embed"
 	"fmt"
+	"io/fs"
 	"net"
 	"net/url"
 	"os"
@@ -101,7 +102,7 @@ func GetDockerIds() []string {
 var Config = config.NewConfig(config.WithHostname(GetHostname()))
 
 func LoadConfigFS(fsys afero.Fs) error {
-	if err := Config.Load("", afero.NewIOFS(fsys)); err != nil {
+	if err := Config.Load("", NewRootFS(fsys)); err != nil {
 		if errors.Is(err, os.ErrNotExist) {
 			CmdSuggestion = fmt.Sprintf("Have you set up the project with %s?", Aqua("supabase init"))
 		}
@@ -111,6 +112,19 @@ func LoadConfigFS(fsys afero.Fs) error {
 	return nil
 }
 
+// Adapts fs.FS to support absolute paths
+type rootFS struct {
+	fsys afero.Fs
+}
+
+func (f *rootFS) Open(name string) (fs.File, error) {
+	return f.fsys.Open(name)
+}
+
+func NewRootFS(fsys afero.Fs) fs.FS {
+	return &rootFS{fsys: fsys}
+}
+
 func ToRealtimeEnv(addr config.AddressFamily) string {
 	if addr == config.AddressIPv6 {
 		return "-proto_dist inet6_tcp"
diff --git a/internal/utils/docker.go b/internal/utils/docker.go
index 3d80f8f5..8a44c75d 100644
--- a/internal/utils/docker.go
+++ b/internal/utils/docker.go
@@ -33,6 +33,7 @@ import (
 	"github.com/docker/docker/pkg/stdcopy"
 	"github.com/go-errors/errors"
 	"github.com/spf13/viper"
+	"go.opentelemetry.io/otel"
 )
 
 var Docker = NewDocker()
@@ -43,6 +44,10 @@ func NewDocker() *client.Client {
 	if err != nil {
 		log.Fatalln("Failed to create Docker client:", err)
 	}
+	// Silence otel errors as users don't care about docker metrics
+	// 2024/08/12 23:11:12 1 errors occurred detecting resource:
+	// 	* conflicting Schema URL: https://opentelemetry.io/schemas/1.21.0
+	otel.SetErrorHandler(otel.ErrorHandlerFunc(func(cause error) {}))
 	if err := cli.Initialize(&dockerFlags.ClientOptions{}); err != nil {
 		log.Fatalln("Failed to initialize Docker client:", err)
 	}
diff --git a/internal/utils/flags/db_url.go b/internal/utils/flags/db_url.go
index 629e0332..c183aa1a 100644
--- a/internal/utils/flags/db_url.go
+++ b/internal/utils/flags/db_url.go
@@ -107,6 +107,8 @@ func getPassword(projectRef string) string {
 	if password, err := credentials.Get(projectRef); err == nil {
 		return password
 	}
+	resetUrl := fmt.Sprintf("%s/project/%s/settings/database", utils.GetSupabaseDashboardURL(), projectRef)
+	fmt.Fprintln(os.Stderr, "Forgot your password? Reset it from the Dashboard:", utils.Bold(resetUrl))
 	fmt.Fprint(os.Stderr, "Enter your database password: ")
 	return credentials.PromptMasked(os.Stdin)
 }
diff --git a/internal/utils/tenant/postgrest.go b/internal/utils/tenant/postgrest.go
index 3872dffe..cb8c96d2 100644
--- a/internal/utils/tenant/postgrest.go
+++ b/internal/utils/tenant/postgrest.go
@@ -34,6 +34,6 @@ func (t *TenantAPI) GetPostgrestVersion(ctx context.Context) (string, error) {
 	if len(data.Info.Version) == 0 {
 		return "", errors.New(errPostgrestVersion)
 	}
-	parts := strings.Split(data.Info.Version, " ")
+	parts := strings.Fields(data.Info.Version)
 	return "v" + parts[0], nil
 }
diff --git a/pkg/config/config.go b/pkg/config/config.go
index c602f033..2af488f6 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -211,6 +211,7 @@ type (
 		Public           *bool       `toml:"public"`
 		FileSizeLimit    sizeInBytes `toml:"file_size_limit"`
 		AllowedMimeTypes []string    `toml:"allowed_mime_types"`
+		ObjectsPath      string      `toml:"objects_path"`
 	}
 
 	imageTransformation struct {
@@ -455,7 +456,8 @@ func NewConfig(editors ...ConfigEditor) config {
 				"notion":        {},
 				"twitch":        {},
 				"twitter":       {},
-				"slack":         {},
+				"slack":         {}, // TODO: remove this field in v2
+				"slack_oidc":    {},
 				"spotify":       {},
 				"workos":        {},
 				"zoom":          {},
diff --git a/pkg/config/constants.go b/pkg/config/constants.go
index 04e112d5..4a408b1b 100644
--- a/pkg/config/constants.go
+++ b/pkg/config/constants.go
@@ -12,11 +12,11 @@ const (
 	pgmetaImage      = "supabase/postgres-meta:v0.83.2"
 	studioImage      = "supabase/studio:20240729-ce42139"
 	imageProxyImage  = "darthsim/imgproxy:v3.8.0"
-	edgeRuntimeImage = "supabase/edge-runtime:v1.55.0"
+	edgeRuntimeImage = "supabase/edge-runtime:v1.56.0"
 	vectorImage      = "timberio/vector:0.28.1-alpine"
 	supavisorImage   = "supabase/supavisor:1.1.56"
-	gotrueImage      = "supabase/gotrue:v2.151.0"
-	realtimeImage    = "supabase/realtime:v2.29.15"
+	gotrueImage      = "supabase/gotrue:v2.157.1"
+	realtimeImage    = "supabase/realtime:v2.30.23"
 	storageImage     = "supabase/storage-api:v1.0.6"
 	logflareImage    = "supabase/logflare:1.4.0"
 	// Append to JobImages when adding new dependencies below
diff --git a/pkg/config/templates/config.toml b/pkg/config/templates/config.toml
index e9545a6d..57398279 100644
--- a/pkg/config/templates/config.toml
+++ b/pkg/config/templates/config.toml
@@ -78,6 +78,7 @@ enabled = true
 # public = false
 # file_size_limit = "50MiB"
 # allowed_mime_types = ["image/png", "image/jpeg"]
+# objects_path = "./images"
 
 [auth]
 enabled = true
diff --git a/internal/utils/queue.go b/pkg/queue/queue.go
similarity index 98%
rename from internal/utils/queue.go
rename to pkg/queue/queue.go
index b9dffbcf..e427e01c 100644
--- a/internal/utils/queue.go
+++ b/pkg/queue/queue.go
@@ -1,4 +1,4 @@
-package utils
+package queue
 
 import (
 	"sync"
diff --git a/pkg/storage/batch.go b/pkg/storage/batch.go
index 51065825..8a681242 100644
--- a/pkg/storage/batch.go
+++ b/pkg/storage/batch.go
@@ -3,9 +3,14 @@ package storage
 import (
 	"context"
 	"fmt"
+	"io/fs"
 	"os"
+	"path"
+	"path/filepath"
 
+	"github.com/go-errors/errors"
 	"github.com/supabase/cli/pkg/config"
+	"github.com/supabase/cli/pkg/queue"
 )
 
 func (s *StorageAPI) UpsertBuckets(ctx context.Context, bucketConfig config.BucketConfig, filter ...func(string) bool) error {
@@ -50,3 +55,59 @@ func (s *StorageAPI) UpsertBuckets(ctx context.Context, bucketConfig config.Buck
 	}
 	return nil
 }
+
+type UploadOptions struct {
+	MaxConcurrency uint
+	KeyPrefix      string
+}
+
+func (s *StorageAPI) UpsertObjects(ctx context.Context, bucketConfig config.BucketConfig, fsys fs.FS, opts ...func(*UploadOptions)) error {
+	uo := UploadOptions{MaxConcurrency: 5}
+	for _, apply := range opts {
+		apply(&uo)
+	}
+	jq := queue.NewJobQueue(uo.MaxConcurrency)
+	for name, bucket := range bucketConfig {
+		localPath := bucket.ObjectsPath
+		if len(localPath) == 0 {
+			continue
+		}
+		upload := func(filePath string, info fs.DirEntry, err error) error {
+			if err != nil {
+				return errors.New(err)
+			}
+			if !info.Type().IsRegular() {
+				return nil
+			}
+			dstPath := uo.KeyPrefix
+			relPath, err := filepath.Rel(localPath, filePath)
+			if err != nil {
+				return errors.Errorf("failed to resolve relative path: %w", err)
+			} else if relPath == "." {
+				// Copying single file
+				dstPath = path.Join(name, info.Name())
+			} else {
+				dstPath = path.Join(name, filepath.ToSlash(relPath))
+			}
+			fmt.Fprintln(os.Stderr, "Uploading:", filePath, "=>", dstPath)
+			job := func() error {
+				f, err := fsys.Open(filePath)
+				if err != nil {
+					return errors.Errorf("failed to open file: %w", err)
+				}
+				defer f.Close()
+				fo, err := ParseFileOptions(f)
+				if err != nil {
+					return err
+				}
+				fo.Overwrite = true
+				return s.UploadObjectStream(ctx, dstPath, f, *fo)
+			}
+			return jq.Put(job)
+		}
+		if err := fs.WalkDir(fsys, localPath, upload); err != nil {
+			return err
+		}
+	}
+	return jq.Collect()
+}
diff --git a/pkg/storage/objects.go b/pkg/storage/objects.go
index 4a6ab64a..f048cecf 100644
--- a/pkg/storage/objects.go
+++ b/pkg/storage/objects.go
@@ -3,7 +3,9 @@ package storage
 import (
 	"context"
 	"io"
+	"io/fs"
 	"net/http"
+	"os"
 	"path"
 	"strings"
 
@@ -56,9 +58,10 @@ func (s *StorageAPI) ListObjects(ctx context.Context, bucket, prefix string, pag
 type FileOptions struct {
 	CacheControl string
 	ContentType  string
+	Overwrite    bool
 }
 
-func ParseFileOptions(f afero.File, opts ...func(*FileOptions)) (*FileOptions, error) {
+func ParseFileOptions(f fs.File, opts ...func(*FileOptions)) (*FileOptions, error) {
 	// Customise file options
 	fo := &FileOptions{}
 	for _, apply := range opts {
@@ -76,9 +79,9 @@ func ParseFileOptions(f afero.File, opts ...func(*FileOptions)) (*FileOptions, e
 			return nil, errors.Errorf("failed to read file: %w", err)
 		}
 		fo.ContentType = http.DetectContentType(buf)
-		// TODO: handle cases where file is not seekable
-		_, err = f.Seek(0, io.SeekStart)
-		if err != nil {
+		if s, ok := f.(io.Seeker); !ok {
+			return nil, errors.Errorf("file is not seekable")
+		} else if _, err = s.Seek(0, io.SeekStart); err != nil {
 			return nil, errors.Errorf("failed to seek file: %w", err)
 		}
 	}
@@ -95,13 +98,24 @@ func (s *StorageAPI) UploadObject(ctx context.Context, remotePath, localPath str
 	if err != nil {
 		return err
 	}
+	return s.UploadObjectStream(ctx, remotePath, f, *fo)
+}
+
+func (s *StorageAPI) UploadObjectStream(ctx context.Context, remotePath string, localFile io.Reader, fo FileOptions) error {
 	headers := func(req *http.Request) {
-		req.Header.Add("Content-Type", fo.ContentType)
-		req.Header.Add("Cache-Control", fo.CacheControl)
+		if len(fo.ContentType) > 0 {
+			req.Header.Add("Content-Type", fo.ContentType)
+		}
+		if len(fo.CacheControl) > 0 {
+			req.Header.Add("Cache-Control", fo.CacheControl)
+		}
+		if fo.Overwrite {
+			req.Header.Add("x-upsert", "true")
+		}
 	}
 	// Prepare request
 	remotePath = strings.TrimPrefix(remotePath, "/")
-	resp, err := s.Send(ctx, http.MethodPost, "/storage/v1/object/"+remotePath, f, headers)
+	resp, err := s.Send(ctx, http.MethodPost, "/storage/v1/object/"+remotePath, io.NopCloser(localFile), headers)
 	if err != nil {
 		return err
 	}
@@ -110,16 +124,23 @@ func (s *StorageAPI) UploadObject(ctx context.Context, remotePath, localPath str
 }
 
 func (s *StorageAPI) DownloadObject(ctx context.Context, remotePath, localPath string, fsys afero.Fs) error {
+	f, err := fsys.OpenFile(localPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
+	if err != nil {
+		return errors.Errorf("failed to create file: %w", err)
+	}
+	defer f.Close()
+	return s.DownloadObjectStream(ctx, remotePath, f)
+}
+
+func (s *StorageAPI) DownloadObjectStream(ctx context.Context, remotePath string, localFile io.Writer) error {
 	remotePath = strings.TrimPrefix(remotePath, "/")
 	resp, err := s.Send(ctx, http.MethodGet, "/storage/v1/object/"+remotePath, nil)
 	if err != nil {
 		return err
 	}
 	defer resp.Body.Close()
-	if err := afero.WriteReader(fsys, localPath, resp.Body); err != nil {
-		return errors.Errorf("failed to write file: %w", err)
-	}
-	return nil
+	_, err = io.Copy(localFile, resp.Body)
+	return err
 }
 
 type MoveObjectRequest struct {


Test patch:
diff --git a/internal/db/reset/reset_test.go b/internal/db/reset/reset_test.go
index 3c1acd0e..38fef4b9 100644
--- a/internal/db/reset/reset_test.go
+++ b/internal/db/reset/reset_test.go
@@ -225,7 +225,8 @@ func TestRestartDatabase(t *testing.T) {
 		utils.StorageId = "test-storage"
 		utils.GotrueId = "test-auth"
 		utils.RealtimeId = "test-realtime"
-		for _, container := range []string{utils.StorageId, utils.GotrueId, utils.RealtimeId} {
+		utils.PoolerId = "test-pooler"
+		for _, container := range listServicesToRestart() {
 			gock.New(utils.Docker.DaemonHost()).
 				Post("/v" + utils.Docker.ClientVersion() + "/containers/" + container + "/restart").
 				Reply(http.StatusOK)
@@ -259,11 +260,15 @@ func TestRestartDatabase(t *testing.T) {
 		utils.StorageId = "test-storage"
 		utils.GotrueId = "test-auth"
 		utils.RealtimeId = "test-realtime"
+		utils.PoolerId = "test-pooler"
 		for _, container := range []string{utils.StorageId, utils.GotrueId, utils.RealtimeId} {
 			gock.New(utils.Docker.DaemonHost()).
 				Post("/v" + utils.Docker.ClientVersion() + "/containers/" + container + "/restart").
 				Reply(http.StatusServiceUnavailable)
 		}
+		gock.New(utils.Docker.DaemonHost()).
+			Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.PoolerId + "/restart").
+			Reply(http.StatusNotFound)
 		// Run test
 		err := RestartDatabase(context.Background(), io.Discard)
 		// Check error
diff --git a/internal/seed/buckets/buckets_test.go b/internal/seed/buckets/buckets_test.go
index 1d0e5f5e..e14a40e7 100644
--- a/internal/seed/buckets/buckets_test.go
+++ b/internal/seed/buckets/buckets_test.go
@@ -3,10 +3,12 @@ package buckets
 import (
 	"context"
 	"net/http"
+	"path/filepath"
 	"testing"
 
 	"github.com/BurntSushi/toml"
 	"github.com/h2non/gock"
+	"github.com/spf13/afero"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/supabase/cli/internal/testing/apitest"
@@ -23,6 +25,10 @@ public = true
 [private]
 public = false`
 		require.NoError(t, toml.Unmarshal([]byte(config), &utils.Config.Storage.Buckets))
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		bucketPath := filepath.Join(utils.SupabaseDirPath, "images")
+		require.NoError(t, fsys.Mkdir(bucketPath, 0755))
 		// Setup mock api
 		gock.New(utils.Config.Api.ExternalUrl).
 			Get("/storage/v1/bucket").
@@ -40,7 +46,7 @@ public = false`
 			Reply(http.StatusOK).
 			JSON(storage.CreateBucketResponse{Name: "private"})
 		// Run test
-		err := Run(context.Background(), "", utils.NewConsole())
+		err := Run(context.Background(), "", false, fsys)
 		// Check error
 		assert.NoError(t, err)
 		assert.Empty(t, apitest.ListUnmatchedRequests())
@@ -53,7 +59,7 @@ public = false`
 			Reply(http.StatusOK).
 			JSON([]storage.BucketResponse{})
 		// Run test
-		err := Run(context.Background(), "", nil)
+		err := Run(context.Background(), "", false, afero.NewMemMapFs())
 		// Check error
 		assert.NoError(t, err)
 		assert.Empty(t, apitest.ListUnmatchedRequests())
diff --git a/internal/storage/cp/cp_test.go b/internal/storage/cp/cp_test.go
index a310a351..fe988c21 100644
--- a/internal/storage/cp/cp_test.go
+++ b/internal/storage/cp/cp_test.go
@@ -311,7 +311,7 @@ func TestUploadAll(t *testing.T) {
 			Get("/storage/v1/bucket").
 			Reply(http.StatusServiceUnavailable)
 		// Run test
-		err := UploadStorageObjectAll(context.Background(), mockApi, "", ".", 1, fsys)
+		err := UploadStorageObjectAll(context.Background(), mockApi, "missing", ".", 1, fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Error status 503:")
 		assert.Empty(t, apitest.ListUnmatchedRequests())
diff --git a/pkg/config/testdata/config.toml b/pkg/config/testdata/config.toml
index 02d8c37b..b08879b7 100644
--- a/pkg/config/testdata/config.toml
+++ b/pkg/config/testdata/config.toml
@@ -78,6 +78,7 @@ enabled = false
 public = false
 file_size_limit = "50MiB"
 allowed_mime_types = ["image/png", "image/jpeg"]
+objects_path = "./images"
 
 [auth]
 enabled = true