Status retrieves the status of an export. Assembles status from metadata's NodeAssignments + per-node status files.
(ctx context.Context, principal *models.Principal, backend, id string)
| 234 | // Status retrieves the status of an export. |
| 235 | // Assembles status from metadata's NodeAssignments + per-node status files. |
| 236 | func (s *Scheduler) Status(ctx context.Context, principal *models.Principal, backend, id string) (*models.ExportStatusResponse, error) { |
| 237 | if !s.exportConfig.Enabled.Get() { |
| 238 | return nil, ErrExportDisabled |
| 239 | } |
| 240 | if err := validateExportID(id); err != nil { |
| 241 | return nil, err |
| 242 | } |
| 243 | bucket, path, err := s.validateStorageConfig(backend) |
| 244 | if err != nil { |
| 245 | return nil, err |
| 246 | } |
| 247 | backendStore, err := s.backends.BackupBackend(backend, modulecapabilities.BackendUseCaseExport) |
| 248 | if err != nil { |
| 249 | return nil, fmt.Errorf("%w: backend %s not available: %w", ErrExportValidation, backend, err) |
| 250 | } |
| 251 | |
| 252 | if err := backendStore.Initialize(ctx, id, bucket, path); err != nil { |
| 253 | return nil, fmt.Errorf("%w: initialize backend: %w", ErrExportValidation, err) |
| 254 | } |
| 255 | |
| 256 | // Metadata is always written before the export starts. |
| 257 | meta, err := s.getExportMetadata(ctx, backendStore, id, bucket, path) |
| 258 | if err != nil { |
| 259 | if errors.As(err, &backup.ErrNotFound{}) { |
| 260 | return nil, ErrExportNotFound |
| 261 | } |
| 262 | return nil, fmt.Errorf("get export metadata: %w", err) |
| 263 | } |
| 264 | |
| 265 | // Authorize using metadata classes. |
| 266 | if err := s.authorizer.Authorize(ctx, principal, authorization.READ, authorization.Backups(meta.Classes...)...); err != nil { |
| 267 | return nil, fmt.Errorf("authorization failed: %w", err) |
| 268 | } |
| 269 | |
| 270 | // Terminal metadata — return directly. |
| 271 | switch meta.Status { |
| 272 | case export.Success, export.Failed, export.Canceled: |
| 273 | return s.statusFromMetadata(backendStore, id, bucket, path, meta) |
| 274 | default: |
| 275 | } |
| 276 | |
| 277 | assembled, _, err := s.assembleStatusFromMetadata(ctx, backendStore, id, bucket, path, meta) |
| 278 | if err != nil { |
| 279 | return nil, err |
| 280 | } |
| 281 | // Promote: if all nodes reached a terminal state, persist the final |
| 282 | // metadata so future Status() calls (and Cancel()) see it directly |
| 283 | // without re-assembling from per-node files. |
| 284 | // NOTE: this may race with Participant.tryPromoteMetadata which does the |
| 285 | // same write. Both paths assemble from the same per-node status files so |
| 286 | // the result is identical — last writer wins with the same data. |
| 287 | switch export.Status(assembled.Status) { |
| 288 | case export.Success, export.Failed: |
| 289 | promotedMeta := &ExportMetadata{ |
| 290 | ID: meta.ID, |
| 291 | Backend: meta.Backend, |
| 292 | StartedAt: meta.StartedAt, |
| 293 | CompletedAt: time.Time(assembled.CompletedAt), |