using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using w4c_workflows.Filters;
using w4c_workflows.Services;
namespace w4c_workflows.Controllers;
///
/// Workflow file management endpoints. Provides CRUD operations for the
/// workflow YAML definitions and their sibling code files (shell scripts,
/// JS, Python, etc.) that live in the tenant's per-tenant workflow repository.
///
/// The repository is the single source of truth for a tenant's workflow content:
/// the workflows module edits files here and POST /api/workflows/sync
/// recompiles them (see ). Edits land in the
/// SAME directory reads from, so the "edit in the
/// workflows module" and "the engine runs it" paths are always coherent.
///
/// Works in both storage modes:
/// - Forgejo-backed (Forgejo:AdminToken set): files live in a private
/// per-user Forgejo repo {login}/workflows-{login}, cloned locally.
/// /commit and /pull sync to/from the remote.
/// - Filesystem-only (no AdminToken): files live under
/// {CopiesRoot}/{tenantId}/ with no remote. /commit and
/// /pull are no-ops.
///
[ApiController]
[Route("api/workflow-files")]
public class WorkflowFilesController : ControllerBase
{
public WorkflowFilesController(
WorkflowSourceFactory sourceFactory,
WorkflowRepoStore repoStore,
WorkflowSyncService sync,
RealtimeEventHub events,
ILogger logger)
{
_sourceFactory = sourceFactory;
_repoStore = repoStore;
_sync = sync;
_events = events;
_logger = logger;
}
private readonly WorkflowSourceFactory _sourceFactory;
private readonly WorkflowRepoStore _repoStore;
private readonly WorkflowSyncService _sync;
private readonly RealtimeEventHub _events;
private readonly ILogger _logger;
private string TenantId => (string?)HttpContext.Items["TenantId"]
?? throw new InvalidOperationException("TenantId not resolved by auth middleware");
///
/// The user's Forgejo login when the middleware resolved it (Forgejo-backed
/// mode succeeded). Null when in filesystem-only mode OR when the Forgejo
/// login could not be resolved (the request then falls back to the filesystem
/// source, so file CRUD must target the filesystem dir to stay consistent).
///
private string? MaybeForgejoLogin =>
HttpContext.Items.TryGetValue("ForgejoLogin", out var v) ? v as string : null;
///
/// The on-disk directory holding the tenant's workflow files. In Forgejo-backed
/// mode (login resolved) it is the per-user clone dir; otherwise it is the
/// per-tenant directory. Both are exactly the root
/// reads from, so file edits made through these endpoints are what get compiled
/// on sync.
///
private string SourceDir => _sourceFactory.ResolveTenantSourceDir(
TenantId, MaybeForgejoLogin, CurrentRepo);
/// The tenant's current workflow repo name (set by the source middleware).
private string CurrentRepo =>
(HttpContext.Items["WorkflowRepo"] as string)
?? (_sourceFactory.Forgejo?.WorkflowRepoName ?? "workflows");
/// Returns the Forgejo repo info for this tenant's workflow files.
[HttpGet("repo")]
[RequireScope("read")]
public IActionResult GetRepoInfo()
{
if (_sourceFactory.Forgejo == null)
return StatusCode(503, new { error = "Forgejo-backed workflow mode is not configured." });
var login = MaybeForgejoLogin;
if (string.IsNullOrWhiteSpace(login))
return StatusCode(503, new { error = "Forgejo login not resolved for this tenant." });
var repoName = _repoStore.DefaultName;
try { repoName = CurrentRepo; } catch { /* keep default */ }
var cloneDir = _sourceFactory.Forgejo.TenantCloneDir(TenantId, login, repoName);
var fullName = _sourceFactory.Forgejo.RepoFullNameForLogin(login, repoName);
var exists = Directory.Exists(Path.Combine(cloneDir, ".git"));
return Ok(new
{
repoName,
repoFullName = fullName,
cloneDir,
cloned = exists,
forgejoUrl = $"{_sourceFactory.Forgejo.ForgejoBaseUrl}/{fullName}",
});
}
///
/// Sets the tenant's workflow repository (basename, e.g. workflows or
/// a user-selected repo) and re-syncs. Switching repos unloads workflows
/// compiled from any previously-selected repo (they are no longer listed).
///
[HttpPut("repo")]
[RequireScope("manage")]
public async Task SetRepo([FromBody] SetWorkflowRepoRequest? request, CancellationToken ct)
{
if (request == null || string.IsNullOrWhiteSpace(request.RepoName))
return BadRequest(new { error = "repoName is required" });
var repoName = WorkflowRepoStore.NormalizeName(request.RepoName);
await _repoStore.SetNameAsync(TenantId, repoName, ct);
// Re-sync workflows for the newly selected repo so the list reflects it.
var syncResult = await _sync.SyncAsync(TenantId, repoName, ct);
var login = MaybeForgejoLogin;
string? fullName = null, cloneDir = null;
if (_sourceFactory.Forgejo != null && !string.IsNullOrWhiteSpace(login))
{
cloneDir = _sourceFactory.Forgejo.TenantCloneDir(TenantId, login, repoName);
fullName = _sourceFactory.Forgejo.RepoFullNameForLogin(login, repoName);
}
return Ok(new
{
repoName,
repoFullName = fullName,
cloneDir,
compiled = syncResult.Compiled,
removed = syncResult.Removed,
errors = syncResult.Errors,
});
}
///
/// Lists all files under a directory (recursive, excluding .git), plus the
/// top-level subdirectories. Returns repo-relative paths.
///
[HttpGet]
[RequireScope("read")]
public IActionResult ListFiles([FromQuery] string? path = null)
{
var sourceDir = SourceDir;
if (!Directory.Exists(sourceDir))
return Ok(new { files = Array.Empty