// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using Horde.Build.Acls; using Horde.Build.Configuration; using Horde.Build.Server; using Horde.Build.Utilities; using Microsoft.Extensions.Logging; using MongoDB.Bson.Serialization.Attributes; using MongoDB.Driver; namespace Horde.Build.Projects { using ProjectId = StringId; /// /// Wraps functionality for manipulating projects /// public class ProjectCollection : IProjectCollection { /// /// Represents a project /// class ProjectDocument : IProject { public const int DefaultOrder = 128; [BsonRequired, BsonId] public ProjectId Id { get; set; } [BsonRequired] public string ConfigRevision { get; set; } = null!; public int Order { get; set; } = DefaultOrder; public Acl? Acl { get; set; } [BsonIgnoreIfDefault, BsonDefaultValue(false)] public bool Deleted { get; set; } #region IProject interface [BsonIgnore] public string Name => Config?.Name!; [BsonIgnore] public ProjectConfig Config { get; private set; } = null!; public async Task PostLoadAsync(ConfigCollection configCollection, ILogger logger) { try { if (Deleted) { Config = new ProjectConfig(); } else { Config = await configCollection.GetConfigAsync(ConfigRevision); } } catch (Exception) { logger.LogError("Unable to get stream config for {StreamId} at {Revision}", Id, ConfigRevision); throw; } } #endregion } /// /// Logo for a project /// class ProjectLogoDocument : IProjectLogo { public ProjectId Id { get; set; } public string Path { get; set; } = String.Empty; public string Revision { get; set; } = String.Empty; public string MimeType { get; set; } = String.Empty; public byte[] Data { get; set; } = Array.Empty(); } /// /// Projection of a project definition to just include permissions info /// [SuppressMessage("Design", "CA1812: Class is never instantiated")] class ProjectPermissions : IProjectPermissions { /// /// ACL for the project /// public Acl? Acl { get; set; } /// /// Projection to extract the permissions info from the project /// public static readonly ProjectionDefinition Projection = Builders.Projection.Include(x => x.Acl); } readonly IMongoCollection _projects; readonly IMongoCollection _projectLogos; readonly ConfigCollection _configCollection; readonly ILogger _logger; /// /// Constructor /// public ProjectCollection(MongoService mongoService, ConfigCollection configCollection, ILogger logger) { _projects = mongoService.GetCollection("Projects"); _projectLogos = mongoService.GetCollection("ProjectLogos"); _configCollection = configCollection; _logger = logger; } /// public async Task AddOrUpdateAsync(ProjectId id, string configRevision, int order) { ProjectConfig config = await _configCollection.GetConfigAsync(configRevision); ProjectDocument newProject = new ProjectDocument(); newProject.Id = id; newProject.ConfigRevision = configRevision; newProject.Order = order; newProject.Acl = Acl.Merge(new Acl(), config.Acl); ProjectDocument result = await _projects.FindOneAndReplaceAsync(x => x.Id == id, newProject, new FindOneAndReplaceOptions { IsUpsert = true, ReturnDocument = ReturnDocument.After }); await result.PostLoadAsync(_configCollection, _logger); return result; } /// public async Task> FindAllAsync() { List results = await _projects.Find(x => !x.Deleted).ToListAsync(); foreach (ProjectDocument result in results) { await result.PostLoadAsync(_configCollection, _logger); } return results.OrderBy(x => x.Order).ThenBy(x => x.Name).Select(x => x).ToList(); } /// public async Task GetAsync(ProjectId projectId) { ProjectDocument? project = await _projects.Find(x => x.Id == projectId).FirstOrDefaultAsync(); if (project != null) { await project.PostLoadAsync(_configCollection, _logger); } return project; } /// public async Task GetLogoAsync(ProjectId projectId) { return await _projectLogos.Find(x => x.Id == projectId).FirstOrDefaultAsync(); } /// public async Task SetLogoAsync(ProjectId projectId, string logoPath, string logoRevision, string mimeType, byte[] data) { ProjectLogoDocument logo = new ProjectLogoDocument(); logo.Id = projectId; logo.Path = logoPath; logo.Revision = logoRevision; logo.MimeType = mimeType; logo.Data = data; await _projectLogos.ReplaceOneAsync(x => x.Id == projectId, logo, new ReplaceOptions { IsUpsert = true }); } /// public async Task GetPermissionsAsync(ProjectId projectId) { return await _projects.Find(x => x.Id == projectId).Project(ProjectPermissions.Projection).FirstOrDefaultAsync(); } /// public async Task DeleteAsync(ProjectId projectId) { await _projects.UpdateOneAsync(x => x.Id == projectId, Builders.Update.Set(x => x.Deleted, true)); } } }