Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ Response:
"cached_artifacts": 142,
"total_size_bytes": 523456789,
"total_size": "499.2 MB",
"storage_path": "./cache/artifacts",
"storage_url": "file:///path/to/cache/artifacts",
"database_path": "./cache/proxy.db"
}
```
Expand Down
2 changes: 1 addition & 1 deletion docs/swagger/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -939,7 +939,7 @@ const docTemplate = `{
"database_path": {
"type": "string"
},
"storage_path": {
"storage_url": {
"type": "string"
},
"total_size": {
Expand Down
2 changes: 1 addition & 1 deletion docs/swagger/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -932,7 +932,7 @@
"database_path": {
"type": "string"
},
"storage_path": {
"storage_url": {
"type": "string"
},
"total_size": {
Expand Down
4 changes: 4 additions & 0 deletions internal/handler/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ func (s *mockStorage) UsedSpace(_ context.Context) (int64, error) {
return total, nil
}

func (s *mockStorage) URL() string { return "mem://" }

func (s *mockStorage) Close() error { return nil }

// mockFetcher implements fetch.FetcherInterface for testing.
type mockFetcher struct {
artifact *fetch.Artifact
Expand Down
22 changes: 19 additions & 3 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,19 @@ func New(cfg *config.Config, logger *slog.Logger) (*Server, error) {
return nil, fmt.Errorf("initializing storage: %w", err)
}

// Verify storage is accessible (catches bad S3 credentials/endpoints early).
// Exists returns (false, nil) for a missing key, so only real connectivity
// or permission errors surface here.
if _, err := store.Exists(context.Background(), ".health-check"); err != nil {
_ = store.Close()
_ = db.Close()
return nil, fmt.Errorf("verifying storage connectivity: %w", err)
}

// Load templates
templates, err := NewTemplates()
if err != nil {
_ = store.Close()
_ = db.Close()
return nil, fmt.Errorf("loading templates: %w", err)
}
Expand Down Expand Up @@ -244,7 +254,7 @@ func (s *Server) Start() error {
s.logger.Info("starting server",
"listen", s.cfg.Listen,
"base_url", s.cfg.BaseURL,
"storage", s.cfg.Storage.Path, //nolint:staticcheck // backwards compat
"storage", s.storage.URL(),
"database", s.cfg.Database.Path)

// Start background goroutine to update cache stats metrics
Expand Down Expand Up @@ -287,6 +297,12 @@ func (s *Server) Shutdown(ctx context.Context) error {
}
}

if s.storage != nil {
if err := s.storage.Close(); err != nil {
errs = append(errs, fmt.Errorf("storage close: %w", err))
}
}

if s.db != nil {
if err := s.db.Close(); err != nil {
errs = append(errs, fmt.Errorf("database close: %w", err))
Expand Down Expand Up @@ -707,7 +723,7 @@ type StatsResponse struct {
CachedArtifacts int64 `json:"cached_artifacts"`
TotalSize int64 `json:"total_size_bytes"`
TotalSizeHuman string `json:"total_size"`
StoragePath string `json:"storage_path"`
StorageURL string `json:"storage_url"`
DatabasePath string `json:"database_path"`
}

Expand Down Expand Up @@ -739,7 +755,7 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
CachedArtifacts: count,
TotalSize: size,
TotalSizeHuman: formatSize(size),
StoragePath: s.cfg.Storage.Path, //nolint:staticcheck // backwards compat
StorageURL: s.storage.URL(),
DatabasePath: s.cfg.Database.Path,
}

Expand Down
57 changes: 57 additions & 0 deletions internal/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,10 @@ func TestStatsEndpoint(t *testing.T) {
if stats.CachedArtifacts != 0 {
t.Errorf("expected 0 cached artifacts, got %d", stats.CachedArtifacts)
}

if !strings.HasPrefix(stats.StorageURL, "file://") {
t.Errorf("expected storage_url to start with file://, got %q", stats.StorageURL)
}
}

func TestDashboard(t *testing.T) {
Expand Down Expand Up @@ -867,3 +871,56 @@ func TestHandlePackagesListPage(t *testing.T) {
t.Error("expected packages list to contain seeded package")
}
}

func TestNewServer_StorageConnectivityCheck(t *testing.T) {
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "test.db")
storagePath := filepath.Join(tempDir, "artifacts")

cfg := &config.Config{
Listen: ":0",
BaseURL: "http://localhost:8080",
Storage: config.StorageConfig{URL: "file://" + storagePath},
Database: config.DatabaseConfig{Path: dbPath},
}

logger := slog.New(slog.NewTextHandler(io.Discard, nil))

srv, err := New(cfg, logger)
if err != nil {
t.Fatalf("New() failed: %v", err)
}

// On Windows, OpenBucket normalises to file:///C:/path; on Unix the
// absolute path already starts with /, so file:// + /path == file:///path.
wantPrefix := "file://"
wantSuffix := filepath.ToSlash(storagePath)
got := srv.storage.URL()
if !strings.HasPrefix(got, wantPrefix) || !strings.HasSuffix(got, wantSuffix) {
t.Errorf("expected storage URL ending with %s, got %s", wantSuffix, got)
}

_ = srv.db.Close()
}

func TestStatsEndpoint_StorageURL(t *testing.T) {
ts := newTestServer(t)
defer ts.close()

req := httptest.NewRequest("GET", "/stats", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)

if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", w.Code)
}

// Verify the JSON response uses storage_url (not storage_path)
body := w.Body.String()
if !strings.Contains(body, `"storage_url"`) {
t.Errorf("expected JSON key storage_url in response, got: %s", body)
}
if strings.Contains(body, `"storage_path"`) {
t.Errorf("unexpected JSON key storage_path in response (should be storage_url)")
}
}
8 changes: 8 additions & 0 deletions internal/storage/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,11 @@ func (fs *Filesystem) Root() string {
func (fs *Filesystem) FullPath(path string) string {
return fs.fullPath(path)
}

func (fs *Filesystem) URL() string {
return "file://" + filepath.ToSlash(fs.root)
}

func (fs *Filesystem) Close() error {
return nil
}
6 changes: 6 additions & 0 deletions internal/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ type Storage interface {

// UsedSpace returns the total bytes used by all stored content.
UsedSpace(ctx context.Context) (int64, error)

// URL returns the storage backend URL (e.g. "file:///path" or "s3://bucket").
URL() string

// Close releases any resources held by the storage backend.
Close() error
}

// ArtifactPath builds a storage path for an artifact.
Expand Down