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 <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto committed 2026-11-03 08:38:35 +00:00
1 parent 8d386cfe0e
commit c9643cc429
4 files changed
+28 -33

No files matched your search

+9 -7
View File
@@ -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 {
+15 -24
View File
@@ -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.
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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 {