From c9643cc4295fb5acac732d2ead94275d1416b73e Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Tue, 3 Nov 2026 08:26:27 +0000 Subject: [PATCH] refactor(application): tidy workspace file setup initStudioFiles had a named result it never used, and the setup repeated the same strict-or-disable branch five times. Fold them into one local helper. workspaceStartupCleanup changed options.Context as a side effect and the cancel func of its derived context was only reachable through the cleanup. Return the context and the cleanup, and let the caller assign the context. Assisted-by: Claude Code:claude-sonnet-5-5 Signed-off-by: Ettore Di Giacinto --- core/application/startup.go | 16 ++++---- core/application/studio_files.go | 39 ++++++++------------ core/application/studio_shared_files_test.go | 3 +- core/application/workspace_automatic_test.go | 3 +- 4 files changed, 28 insertions(+), 33 deletions(-) diff --git a/core/application/startup.go b/core/application/startup.go index 1252914d3..aceee15a2 100644 --- a/core/application/startup.go +++ b/core/application/startup.go @@ -42,7 +42,8 @@ import ( func New(opts ...config.AppOption) (*Application, error) { options := config.NewApplicationConfig(opts...) - cleanupWorkspace := workspaceStartupCleanup(options) + ctx, cleanupWorkspace := workspaceStartupCleanup(options.Context, options) + options.Context = ctx // Store a copy of the startup config (env/CLI only, before file // loading): the settings endpoint uses it to tell env-provided API @@ -833,15 +834,16 @@ func ensureAuthDBDirs(options *config.ApplicationConfig) error { // workspaceStartupCleanup owns startup resources when workspace initialization // can fail fatally: configured S3 (with or without auth), or PostgreSQL metadata. -// Never cancel the caller's context or change legacy JSON/SQLite without S3. -func workspaceStartupCleanup(options *config.ApplicationConfig) func(*Application) { +// It returns the context that startup runs in and the cleanup. Without those +// setups it returns ctx itself and a no-op: it never cancels the caller's +// context or changes legacy JSON/SQLite without S3. +func workspaceStartupCleanup(ctx context.Context, options *config.ApplicationConfig) (context.Context, func(*Application)) { postgres := options.Auth.Enabled && (strings.HasPrefix(options.Auth.DatabaseURL, "postgres://") || strings.HasPrefix(options.Auth.DatabaseURL, "postgresql://")) if options.Distributed.StorageURL == "" && !postgres { - return func(*Application) {} + return ctx, func(*Application) {} } - ctx, cancel := context.WithCancel(options.Context) - options.Context = ctx - return func(a *Application) { + ctx, cancel := context.WithCancel(ctx) + return ctx, func(a *Application) { cancel() // stop background work before shutting down its dependencies _ = a.Shutdown() if a.authDB != nil { diff --git a/core/application/studio_files.go b/core/application/studio_files.go index c33eeb1ee..82d2641ac 100644 --- a/core/application/studio_files.go +++ b/core/application/studio_files.go @@ -53,7 +53,7 @@ func (a *Application) StudioFiles() *files.Service { // store; a build without cgo and without any build tag has it too. The bytes // live in configured S3 for every role, otherwise at DataPath/chats/blobs. // Moving files between backends is not supported automatically. -func (a *Application) initStudioFiles() (result error) { +func (a *Application) initStudioFiles() error { return a.initStudioFilesWithS3(func(ctx context.Context, cfg storage.S3Config) (storage.ObjectStore, error) { return storage.NewS3Store(ctx, cfg) }) @@ -76,15 +76,21 @@ func (a *Application) initStudioFilesWithS3(newS3 func(context.Context, storage. a.studioFiles = files.Disabled("There is no data path, so files are off. Start LocalAI with a data path to keep files.") return nil } + // stop ends the setup: a strict setup fails startup, otherwise files are + // off with a reason the user can read. + stop := func(what string, err error, reason string) error { + if strict { + return fmt.Errorf("%s: %w", what, err) + } + xlog.Warn("Files disabled: "+what, "error", err) + a.studioFiles = files.Disabled(reason) + return nil + } var tmpCache string if dp == "" { // a cluster without a data path: the blob cache is a temporary folder dir, err := os.MkdirTemp("", "localai-files-*") if err != nil { - if strict { - return fmt.Errorf("workspace scratch: %w", err) - } - a.studioFiles = files.Disabled("Files are off: there is no folder for their cache.") - return + return stop("workspace scratch", err, "Files are off: there is no folder for their cache.") } a.studioFilesTemp, tmpCache = dir, dir } @@ -100,33 +106,18 @@ func (a *Application) initStudioFilesWithS3(newS3 func(context.Context, storage. blobs, err = blob.NewFS(filepath.Join(root, "blobs")) } if err != nil { - if strict { - return fmt.Errorf("workspace blob store: %w", err) - } - xlog.Warn("Files disabled: cannot open the blob store", "error", err) - a.studioFiles = files.Disabled("Files are off: their storage could not be opened.") - return + return stop("workspace blob store", err, "Files are off: their storage could not be opened.") } ws, closer, err := a.openWorkspaceIndex(root, blobs) if err != nil { - if strict { - return fmt.Errorf("workspace index: %w", err) - } - xlog.Warn("Files disabled: cannot start the workspace", "error", err) - a.studioFiles = files.Disabled("Files are off: the workspace could not be started.") - return + return stop("workspace index", err, "Files are off: the workspace could not be started.") } f := files.New(ws, files.WithBlobs(blobs), files.WithMaxFileSize(a.studioUploadLimit())) if err := f.EnsureShared(context.Background()); err != nil { - xlog.Warn("Files disabled: cannot create the shared folders", "error", err) - a.studioFiles = files.Disabled("Files are off: the workspace could not be started.") if closer != nil { _ = closer.Close() } - if strict { - return fmt.Errorf("workspace folders: %w", err) - } - return + return stop("workspace folders", err, "Files are off: the workspace could not be started.") } // The office skills ship with the server. A copy that fails leaves the // chats as they are: the skills are help, not a need. diff --git a/core/application/studio_shared_files_test.go b/core/application/studio_shared_files_test.go index 57d8bf04c..ed8a0fae4 100644 --- a/core/application/studio_shared_files_test.go +++ b/core/application/studio_shared_files_test.go @@ -128,7 +128,8 @@ var _ = Describe("PostgreSQL shared filesystem startup cleanup", func() { caller, cancel := context.WithCancel(context.Background()) DeferCleanup(cancel) cfg := config.NewApplicationConfig(config.WithContext(caller), config.WithDataPath(GinkgoT().TempDir()), config.WithAuthEnabled(true), config.WithAuthDatabaseURL(raw)) - cleanup := workspaceStartupCleanup(cfg) + ctx, cleanup := workspaceStartupCleanup(cfg.Context, cfg) + cfg.Context = ctx app := &Application{applicationConfig: cfg, authDB: db} if early { cfg.Distributed.StorageURL = "http://s3.invalid" diff --git a/core/application/workspace_automatic_test.go b/core/application/workspace_automatic_test.go index c61748fa4..b03f319fd 100644 --- a/core/application/workspace_automatic_test.go +++ b/core/application/workspace_automatic_test.go @@ -81,7 +81,8 @@ var _ = Describe("automatic workspace", func() { cfg.Distributed.StorageURL = "http://s3.invalid:9000" // no bucket: fail before any network access cfg.Auth.Enabled = database cfg.Auth.DatabaseURL = filepath.Join(GinkgoT().TempDir(), "auth.db") - cleanup := workspaceStartupCleanup(cfg) // the same ownership scope used by New + ctx, cleanup := workspaceStartupCleanup(cfg.Context, cfg) // the same ownership scope used by New + cfg.Context = ctx app := &Application{applicationConfig: cfg} DeferCleanup(func() { Expect(app.Shutdown()).To(Succeed()) }) if database {