back photo + event tags and persons
This commit is contained in:
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
@@ -12,6 +12,14 @@
|
|||||||
"webRoot": "${workspaceFolder}/front/v2",
|
"webRoot": "${workspaceFolder}/front/v2",
|
||||||
"preLaunchTask": "Start Node server with nvs latest"
|
"preLaunchTask": "Start Node server with nvs latest"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "(legacy) FRONT: DEBUG(Edge)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "msedge",
|
||||||
|
"url": "http://localhost:4200",
|
||||||
|
"webRoot": "${workspaceFolder}/front/v1",
|
||||||
|
"preLaunchTask": "(legacy) Start Node server with nvs latest"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Attach Edge",
|
"name": "Attach Edge",
|
||||||
"type": "msedge",
|
"type": "msedge",
|
||||||
|
16
.vscode/tasks.json
vendored
16
.vscode/tasks.json
vendored
@@ -1,6 +1,22 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "(legacy) Start Node server with nvs latest",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "nvs use latest && npm run start",
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}/front/v1"
|
||||||
|
},
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": [],
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "shared"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Start Node server with nvs latest",
|
"label": "Start Node server with nvs latest",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
public static class Constants
|
|
||||||
{
|
|
||||||
public const string Data = "data";
|
|
||||||
}
|
|
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
public class PhotoFormModel
|
public class PhotoFormModel
|
||||||
{
|
{
|
||||||
|
public required string UserId { get; set; }
|
||||||
public required string Title { get; set; }
|
public required string Title { get; set; }
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
public string? Tags { get; set; }
|
public string[]? Tags { get; set; }
|
||||||
public string? People { get; set; }
|
public string[]? People { get; set; }
|
||||||
public IFormFile? Image { get; set; }
|
public IFormFile? Image { get; set; }
|
||||||
public string? Ubicacion { get; set; }
|
public string? Ubicacion { get; set; }
|
||||||
public string? Evento { get; set; }
|
public string? Evento { get; set; }
|
||||||
|
public bool IsPublic { get; set; }
|
||||||
}
|
}
|
||||||
|
52
back/DataModels/EventModel.cs
Normal file
52
back/DataModels/EventModel.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace back.DataModels;
|
||||||
|
|
||||||
|
[Table("Events")]
|
||||||
|
public class EventModel
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public string Id { get; set; }
|
||||||
|
[Required, MaxLength(50)]
|
||||||
|
public string? Title { get; set; }
|
||||||
|
[MaxLength(500)]
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public DateTime? Date { get; set; }
|
||||||
|
public string? Location { get; set; }
|
||||||
|
public List<TagModel> RelatedTags { get; set; } = [];
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
public string? CreatedBy { get; set; }
|
||||||
|
public string? UpdatedBy { get; set; }
|
||||||
|
|
||||||
|
public EventModel(string id)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EventModel(
|
||||||
|
string id,
|
||||||
|
string title,
|
||||||
|
string description,
|
||||||
|
DateTime date,
|
||||||
|
string location,
|
||||||
|
List<TagModel>? relatedTags = null,
|
||||||
|
DateTime? createdAt = null,
|
||||||
|
DateTime? updatedAt = null,
|
||||||
|
string? createdBy = null,
|
||||||
|
string? updatedBy = null)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
Title = title;
|
||||||
|
Description = description;
|
||||||
|
Date = date;
|
||||||
|
Location = location;
|
||||||
|
RelatedTags = relatedTags ?? [];
|
||||||
|
CreatedAt = createdAt ?? DateTime.UtcNow;
|
||||||
|
UpdatedAt = updatedAt ?? DateTime.UtcNow;
|
||||||
|
CreatedBy = createdBy ?? "";
|
||||||
|
UpdatedBy = updatedBy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
27
back/DataModels/GalleryModel.cs
Normal file
27
back/DataModels/GalleryModel.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace back.DataModels;
|
||||||
|
|
||||||
|
[Table("Galleries")]
|
||||||
|
public class GalleryModel
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public string Id { get; set; }
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string? Title { get; set; }
|
||||||
|
[MaxLength(500)]
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public DateTime? CreatedAt { get; set; }
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
public string? CreatedBy { get; set; }
|
||||||
|
public string? UpdatedBy { get; set; }
|
||||||
|
public List<PhotoModel> Photos { get; set; } = new();
|
||||||
|
public bool? IsPublic { get; set; } = true;
|
||||||
|
public bool? IsArchived { get; set; } = false;
|
||||||
|
public bool? IsFavorite { get; set; } = false;
|
||||||
|
public EventModel? Event { get; set; } = null;
|
||||||
|
public List<TagModel>? Tags { get; set; } = null;
|
||||||
|
public List<PersonModel>? PersonsInvolved { get; set; } = null;
|
||||||
|
public List<PersonModel>? UsersWhoCanSee { get; set; } = null;
|
||||||
|
}
|
34
back/DataModels/PermissionModel.cs
Normal file
34
back/DataModels/PermissionModel.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace back.DataModels;
|
||||||
|
|
||||||
|
[Table("Permissions")]
|
||||||
|
public class PermissionModel
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public string Id { get; set; }
|
||||||
|
[Required, MaxLength(100)]
|
||||||
|
public string Name { get; set; }
|
||||||
|
[MaxLength(255)]
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
public PermissionModel(string id, string name, string description)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
Name = name;
|
||||||
|
Description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static permissions
|
||||||
|
public static readonly PermissionModel ViewContentPermission = new("1", "VIEW_CONTENT", "Permission to view content");
|
||||||
|
public static readonly PermissionModel LikeContentPermission = new("2", "LIKE_CONTENT", "Permission to like content");
|
||||||
|
public static readonly PermissionModel EditContentPermission = new("3", "EDIT_CONTENT", "Permission to edit content");
|
||||||
|
public static readonly PermissionModel DeleteContentPermission = new("4", "DELETE_CONTENT", "Permission to delete content");
|
||||||
|
public static readonly PermissionModel CreateContentPermission = new("5", "CREATE_CONTENT", "Permission to create new content");
|
||||||
|
public static readonly PermissionModel EditUserPermission = new("6", "EDIT_USER", "Permission to edit user");
|
||||||
|
public static readonly PermissionModel DeleteUserPermission = new("7", "DELETE_USER", "Permission to delete user");
|
||||||
|
public static readonly PermissionModel DisableUserPermission = new("8", "DISABLE_USER", "Permission to disable user");
|
||||||
|
public static readonly PermissionModel CreateUserPermission = new("9", "CREATE_USER", "Permission to create new user");
|
||||||
|
public static readonly PermissionModel EditWebConfigPermission = new("10", "EDIT_WEB_CONFIG", "Permission to edit web configuration");
|
||||||
|
}
|
49
back/DataModels/PersonModel.cs
Normal file
49
back/DataModels/PersonModel.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace back.DataModels;
|
||||||
|
|
||||||
|
[Table("Persons")]
|
||||||
|
public class PersonModel
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public string Id { get; set; }
|
||||||
|
[Required, MaxLength(100)]
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? ProfilePicture { get; set; }
|
||||||
|
public string? Avatar { get; set; }
|
||||||
|
public SocialMedia? SocialMedia { get; set; }
|
||||||
|
[MaxLength(250)]
|
||||||
|
public string? Bio { get; set; } // Optional field for a short biography or description
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
public PersonModel(string id)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PersonModel(string id, string name, string? profilePicture = null, string? avatar = null, SocialMedia? socialMedia = null)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
Name = name;
|
||||||
|
ProfilePicture = profilePicture;
|
||||||
|
Avatar = avatar;
|
||||||
|
SocialMedia = socialMedia;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Table("SocialMediaLinks")]
|
||||||
|
public class SocialMedia
|
||||||
|
{
|
||||||
|
public string? Facebook { get; set; }
|
||||||
|
public string? Instagram { get; set; }
|
||||||
|
public string? Twitter { get; set; }
|
||||||
|
public string? BlueSky { get; set; }
|
||||||
|
public string? Tiktok { get; set; }
|
||||||
|
public string? Linkedin { get; set; }
|
||||||
|
public string? Pinterest { get; set; }
|
||||||
|
public string? Discord { get; set; }
|
||||||
|
public string? Reddit { get; set; }
|
||||||
|
public string? Other { get; set; }
|
||||||
|
}
|
68
back/DataModels/PhotoModel.cs
Normal file
68
back/DataModels/PhotoModel.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace back.DataModels;
|
||||||
|
|
||||||
|
[Table("Photo")]
|
||||||
|
public class PhotoModel
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public string Id { get; set; }
|
||||||
|
[Required, MaxLength(100), MinLength(1)]
|
||||||
|
public string Title { get; set; }
|
||||||
|
[MaxLength(500)]
|
||||||
|
public string Description { get; set; }
|
||||||
|
public string Extension { get; set; }
|
||||||
|
public string LowResUrl { get; set; }
|
||||||
|
public string MidResUrl { get; set; }
|
||||||
|
public string HighResUrl { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
public string CreatedBy { get; set; }
|
||||||
|
public string UpdatedBy { get; set; }
|
||||||
|
public EventModel? Event { get; set; } = null;
|
||||||
|
public List<TagModel> Tags { get; set; } = [];
|
||||||
|
public RankingModel Ranking { get; set; } = new RankingModel(0);
|
||||||
|
public bool IsFavorite { get; set; } = false;
|
||||||
|
public bool IsPublic { get; set; } = true;
|
||||||
|
public bool IsArchived { get; set; } = false;
|
||||||
|
public List<PersonModel>? Persons { get; set; }
|
||||||
|
|
||||||
|
public PhotoModel(
|
||||||
|
string id,
|
||||||
|
string title,
|
||||||
|
string description,
|
||||||
|
string lowResUrl,
|
||||||
|
string midResUrl,
|
||||||
|
string highResUrl,
|
||||||
|
DateTime createdAt,
|
||||||
|
DateTime updatedAt,
|
||||||
|
string createdBy,
|
||||||
|
string updatedBy,
|
||||||
|
EventModel? @event = null,
|
||||||
|
List<TagModel>? tags = null,
|
||||||
|
RankingModel? ranking = null,
|
||||||
|
bool isFavorite = false,
|
||||||
|
bool isPublic = true,
|
||||||
|
bool isArchived = false,
|
||||||
|
List<PersonModel>? persons = null)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
Title = title;
|
||||||
|
Description = description;
|
||||||
|
LowResUrl = lowResUrl;
|
||||||
|
MidResUrl = midResUrl;
|
||||||
|
HighResUrl = highResUrl;
|
||||||
|
CreatedAt = createdAt;
|
||||||
|
UpdatedAt = updatedAt;
|
||||||
|
CreatedBy = createdBy;
|
||||||
|
UpdatedBy = updatedBy;
|
||||||
|
Event = @event ?? Event;
|
||||||
|
Tags = tags ?? Tags;
|
||||||
|
Ranking = ranking ?? Ranking;
|
||||||
|
IsFavorite = isFavorite;
|
||||||
|
IsPublic = isPublic;
|
||||||
|
IsArchived = isArchived;
|
||||||
|
Persons = persons ?? Persons;
|
||||||
|
}
|
||||||
|
}
|
36
back/DataModels/RankingModel.cs
Normal file
36
back/DataModels/RankingModel.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
namespace back.DataModels;
|
||||||
|
|
||||||
|
public class RankingModel
|
||||||
|
{
|
||||||
|
private int totalVotes;
|
||||||
|
private int upVotes;
|
||||||
|
private int downVotes;
|
||||||
|
|
||||||
|
public RankingModel(int totalVotes, int upVotes = 0, int downVotes = 0)
|
||||||
|
{
|
||||||
|
this.totalVotes = totalVotes;
|
||||||
|
this.upVotes = upVotes;
|
||||||
|
this.downVotes = downVotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DownVote()
|
||||||
|
{
|
||||||
|
downVotes++;
|
||||||
|
totalVotes++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpVote()
|
||||||
|
{
|
||||||
|
upVotes++;
|
||||||
|
totalVotes++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double Score
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (totalVotes == 0) return 0;
|
||||||
|
return (double)(upVotes - downVotes) / totalVotes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
69
back/DataModels/RoleModel.cs
Normal file
69
back/DataModels/RoleModel.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace back.DataModels;
|
||||||
|
|
||||||
|
[Tags("Roles")]
|
||||||
|
public class RoleModel
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public string Id { get; set; }
|
||||||
|
[Required, MaxLength(100)]
|
||||||
|
public string Name { get; set; }
|
||||||
|
[MaxLength(250)]
|
||||||
|
public string Description { get; set; }
|
||||||
|
public List<PermissionModel> Permissions { get; set; }
|
||||||
|
public RoleModel? BaseRoleModel { get; set; }
|
||||||
|
|
||||||
|
public RoleModel(string id, string name, string description, List<PermissionModel>? permissions = null, RoleModel? baseRoleModel = null)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
Name = name;
|
||||||
|
Description = description;
|
||||||
|
Permissions = permissions ?? new List<PermissionModel>();
|
||||||
|
BaseRoleModel = baseRoleModel;
|
||||||
|
if (baseRoleModel != null)
|
||||||
|
{
|
||||||
|
Permissions.AddRange(baseRoleModel.Permissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsAdmin => Id == AdminRole.Id;
|
||||||
|
public bool IsContentManager => Id == ContentManagerRole.Id;
|
||||||
|
public bool IsUser => Id == UserRole.Id;
|
||||||
|
|
||||||
|
public bool HasPermission(PermissionModel permission)
|
||||||
|
{
|
||||||
|
return Permissions.Exists(p => p.Id == permission.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly RoleModel UserRole = new(
|
||||||
|
"1", "User", "Role for regular users",
|
||||||
|
new List<PermissionModel> {
|
||||||
|
PermissionModel.ViewContentPermission,
|
||||||
|
PermissionModel.LikeContentPermission
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly RoleModel ContentManagerRole = new(
|
||||||
|
"2", "Content Manager", "Role for managing content",
|
||||||
|
new List<PermissionModel> {
|
||||||
|
PermissionModel.CreateUserPermission,
|
||||||
|
PermissionModel.DisableUserPermission,
|
||||||
|
PermissionModel.CreateContentPermission,
|
||||||
|
PermissionModel.EditContentPermission,
|
||||||
|
PermissionModel.DeleteContentPermission
|
||||||
|
},
|
||||||
|
UserRole
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly RoleModel AdminRole = new(
|
||||||
|
"3", "Admin", "Administrator role with full permissions",
|
||||||
|
new List<PermissionModel> {
|
||||||
|
PermissionModel.CreateUserPermission,
|
||||||
|
PermissionModel.EditUserPermission,
|
||||||
|
PermissionModel.DeleteUserPermission,
|
||||||
|
PermissionModel.EditWebConfigPermission
|
||||||
|
},
|
||||||
|
ContentManagerRole
|
||||||
|
);
|
||||||
|
}
|
14
back/DataModels/TagModel.cs
Normal file
14
back/DataModels/TagModel.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace back.DataModels;
|
||||||
|
|
||||||
|
[Table("Tags")]
|
||||||
|
public class TagModel(string id, string? name = null)
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public string Id { get; init; } = id;
|
||||||
|
[Required, MaxLength(25)]
|
||||||
|
public string? Name { get; init; } = name;
|
||||||
|
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
|
||||||
|
}
|
38
back/DataModels/UserModel.cs
Normal file
38
back/DataModels/UserModel.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace back.DataModels;
|
||||||
|
|
||||||
|
[Table("Users")]
|
||||||
|
public class UserModel : PersonModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Email { get; set; }
|
||||||
|
[Required, MinLength(8)]
|
||||||
|
public string Password { get; set; }
|
||||||
|
public List<RoleModel> Role { get; set; }
|
||||||
|
|
||||||
|
public UserModel(string id, string email, string password, string name, List<RoleModel> role, DateTime createdAt, DateTime updatedAt)
|
||||||
|
: base(id, name)
|
||||||
|
{
|
||||||
|
Email = email;
|
||||||
|
Password = password;
|
||||||
|
Role = role;
|
||||||
|
CreatedAt = createdAt;
|
||||||
|
UpdatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsAdmin => Role.Exists(r => r.IsAdmin);
|
||||||
|
public bool IsContentManager => Role.Exists(r => r.IsContentManager);
|
||||||
|
public bool IsUser => Role.Exists(r => r.IsUser);
|
||||||
|
|
||||||
|
public static readonly UserModel DefaultUser = new(
|
||||||
|
"0",
|
||||||
|
"default@example.com",
|
||||||
|
string.Empty,
|
||||||
|
"Default User",
|
||||||
|
[RoleModel.UserRole],
|
||||||
|
DateTime.UtcNow,
|
||||||
|
DateTime.UtcNow
|
||||||
|
);
|
||||||
|
}
|
14
back/Options/DatabaseConfig.cs
Normal file
14
back/Options/DatabaseConfig.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace back.Options;
|
||||||
|
|
||||||
|
public sealed class DatabaseConfig
|
||||||
|
{
|
||||||
|
public const string BlobStorage = "Databases:Blob";
|
||||||
|
public const string DataStorage = "Databases:Data";
|
||||||
|
public required string Provider { get; set; }
|
||||||
|
public string? DatabaseName { get; set; }
|
||||||
|
public string? AccountKey { get; set; }
|
||||||
|
public string? TokenCredential { get; set; }
|
||||||
|
public string? BaseUrl { get; set; }
|
||||||
|
public string? ConnectionString { get; set; }
|
||||||
|
public string? SystemContainer { get; set; }
|
||||||
|
}
|
6
back/Options/Databases.cs
Normal file
6
back/Options/Databases.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace back.Options;
|
||||||
|
|
||||||
|
public sealed class Databases
|
||||||
|
{
|
||||||
|
public string? BaseDirectory { get; set; }
|
||||||
|
}
|
@@ -1,5 +1,4 @@
|
|||||||
using back.ApiService.context;
|
using back.ServicesExtensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace back;
|
namespace back;
|
||||||
|
|
||||||
@@ -9,9 +8,8 @@ public class Program
|
|||||||
{
|
{
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
Directory.CreateDirectory(Constants.Data);
|
builder.Services.UseExtensions();
|
||||||
// Add services to the container.
|
builder.Services.AddMemoryCache();
|
||||||
builder.Services.AddDbContext<PhotoContext>(options => options.UseSqlite($"Data Source={Constants.Data}/photos.db"));
|
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||||
|
37
back/ServicesExtensions/DatabaseContexts.cs
Normal file
37
back/ServicesExtensions/DatabaseContexts.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using back.context;
|
||||||
|
using back.Options;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace back.ServicesExtensions;
|
||||||
|
|
||||||
|
public static partial class ServicesExtensions
|
||||||
|
{
|
||||||
|
private static IServiceCollection AddDatabaseContexts(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services
|
||||||
|
.AddContext<EventContext>()
|
||||||
|
.AddContext<GalleryContext>()
|
||||||
|
.AddContext<PersonContext>()
|
||||||
|
.AddContext<PhotoContext>()
|
||||||
|
.AddContext<TagContext>()
|
||||||
|
.AddContext<UserContext>()
|
||||||
|
;
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
private static IServiceCollection AddContext<T>(this IServiceCollection services)
|
||||||
|
where T : DbContext
|
||||||
|
{
|
||||||
|
var config = services
|
||||||
|
.BuildServiceProvider()
|
||||||
|
.GetRequiredService<IOptionsSnapshot<DatabaseConfig>>()
|
||||||
|
.Get(DatabaseConfig.DataStorage);
|
||||||
|
|
||||||
|
services.AddDbContext<T>(options =>
|
||||||
|
{
|
||||||
|
options.UseDatabaseConfig(config);
|
||||||
|
});
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
78
back/ServicesExtensions/DdContextOptionsBuilderExtensions.cs
Normal file
78
back/ServicesExtensions/DdContextOptionsBuilderExtensions.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using back.Options;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace back.ServicesExtensions;
|
||||||
|
|
||||||
|
public enum DatabaseProvider
|
||||||
|
{
|
||||||
|
/* -- Relational databases supported by EF Core -- */
|
||||||
|
SUPPORTED, // Placeholder for supported databases.
|
||||||
|
InMemory,
|
||||||
|
Sqlite,
|
||||||
|
PostgreSQL,
|
||||||
|
CockroachDB, // CockroachDB is compatible with PostgreSQL.
|
||||||
|
SQLServer,
|
||||||
|
MariaDB,
|
||||||
|
MySQL,
|
||||||
|
Oracle, // Oracle is supported by EF Core but requires a separate package.
|
||||||
|
|
||||||
|
/* -- NoSQL are not supported by EF -- */
|
||||||
|
|
||||||
|
NOT_SUPPORTED, // Placeholder for unsupported databases.
|
||||||
|
Firebird, // Firebird is supported by EF Core but requires a separate package.
|
||||||
|
Db2, // Db2 is supported by EF Core but requires a separate package.
|
||||||
|
SAPHana, // SAP HANA is supported by EF Core but requires a separate package.
|
||||||
|
Sybase, // Sybase is supported by EF Core but requires a separate package.
|
||||||
|
Cosmos, // Cosmos DB is database supported by EF Core.
|
||||||
|
MongoDB,
|
||||||
|
InfluxDB,
|
||||||
|
Redis,
|
||||||
|
Cassandra,
|
||||||
|
ElasticSearch,
|
||||||
|
CouchDB,
|
||||||
|
RavenDB,
|
||||||
|
Neo4j,
|
||||||
|
OrientDB,
|
||||||
|
ArangoDB,
|
||||||
|
ClickHouse,
|
||||||
|
Druid,
|
||||||
|
TimescaleDB,
|
||||||
|
}
|
||||||
|
|
||||||
|
public static partial class DbContextOptionsBuilderExtensions
|
||||||
|
{
|
||||||
|
private static string SupportedDbs()
|
||||||
|
=> string.Join(", ", Enum.GetValues<DatabaseProvider>()
|
||||||
|
.Where(db => db > DatabaseProvider.SUPPORTED && db < DatabaseProvider.NOT_SUPPORTED)
|
||||||
|
.OrderBy(db => db)
|
||||||
|
.Select(db => db.ToString()));
|
||||||
|
|
||||||
|
public static void UseDatabaseConfig(this DbContextOptionsBuilder options, DatabaseConfig config)
|
||||||
|
{
|
||||||
|
if(!Enum.TryParse(Enum.GetName(typeof(DatabaseProvider), config.Provider)?.ToLowerInvariant(), out DatabaseProvider provider))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Unsupported database provider: {config.Provider} -- Supported providers are: {SupportedDbs()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (provider)
|
||||||
|
{
|
||||||
|
case DatabaseProvider.Sqlite:
|
||||||
|
options.UseSqlite(config.ConnectionString);
|
||||||
|
break;
|
||||||
|
case DatabaseProvider.InMemory:
|
||||||
|
options.UseInMemoryDatabase(config.ConnectionString);
|
||||||
|
break;
|
||||||
|
case DatabaseProvider.PostgreSQL or DatabaseProvider.CockroachDB:
|
||||||
|
options.UseNpgsql(config.ConnectionString);
|
||||||
|
break;
|
||||||
|
case DatabaseProvider.SQLServer:
|
||||||
|
options.UseSqlServer(config.ConnectionString);
|
||||||
|
break;
|
||||||
|
case DatabaseProvider.MySQL or DatabaseProvider.MariaDB:
|
||||||
|
options.UseMySql(config.ConnectionString, ServerVersion.AutoDetect(config.ConnectionString));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException($"Unsupported database provider: {config.Provider}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
72
back/ServicesExtensions/Options.cs
Normal file
72
back/ServicesExtensions/Options.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
using back.Options;
|
||||||
|
|
||||||
|
namespace back.ServicesExtensions;
|
||||||
|
|
||||||
|
public static partial class ServicesExtensions
|
||||||
|
{
|
||||||
|
private static IConfiguration ConfigureOptions(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
IConfiguration config = services.BuildServiceProvider().GetRequiredService<IConfiguration>();
|
||||||
|
string? baseDirectory = null;
|
||||||
|
|
||||||
|
services.Configure<Databases>(config.GetSection(nameof(Databases)));
|
||||||
|
services.Configure<DatabaseConfig>(DatabaseConfig.DataStorage, config.GetSection(DatabaseConfig.DataStorage));
|
||||||
|
services.Configure<DatabaseConfig>(DatabaseConfig.BlobStorage, config.GetSection(DatabaseConfig.BlobStorage));
|
||||||
|
|
||||||
|
services.PostConfigure<Databases>(databases =>
|
||||||
|
{
|
||||||
|
if (databases.BaseDirectory != null && !Directory.Exists(databases.BaseDirectory))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(databases.BaseDirectory);
|
||||||
|
Console.WriteLine($"Base directory created at: {databases.BaseDirectory}");
|
||||||
|
baseDirectory = databases.BaseDirectory;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Failed to create base directory at {databases.BaseDirectory}. " +
|
||||||
|
"Please ensure the path is valid and accessible.", ex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
services.PostConfigure<DatabaseConfig>(DatabaseConfig.DataStorage, config =>
|
||||||
|
{
|
||||||
|
PostConfigureDatabaseConfig(config, baseDirectory);
|
||||||
|
});
|
||||||
|
|
||||||
|
services.PostConfigure<DatabaseConfig>(DatabaseConfig.BlobStorage, config =>
|
||||||
|
{
|
||||||
|
PostConfigureDatabaseConfig(config, baseDirectory);
|
||||||
|
});
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PostConfigureDatabaseConfig(DatabaseConfig config, string? baseDirectory)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(config.SystemContainer))
|
||||||
|
{
|
||||||
|
var path = config.SystemContainer;
|
||||||
|
if (!string.IsNullOrEmpty(baseDirectory))
|
||||||
|
{
|
||||||
|
path = Path.Combine(baseDirectory, path);
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(path);
|
||||||
|
Console.WriteLine($"System container for {config.Provider} created at: {path}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Failed to create system container at {path}. " +
|
||||||
|
"Please ensure the path is valid and accessible.", ex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
back/ServicesExtensions/ServicesExtensions.cs
Normal file
21
back/ServicesExtensions/ServicesExtensions.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using back.persistance.blob;
|
||||||
|
using back.services.ImageResizer;
|
||||||
|
|
||||||
|
namespace back.ServicesExtensions;
|
||||||
|
|
||||||
|
public static partial class ServicesExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection UseExtensions(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
var config = services.ConfigureOptions();
|
||||||
|
|
||||||
|
services.AddDatabaseContexts();
|
||||||
|
|
||||||
|
// TODO: Move and configure for using S3, Azure Blob Storage, etc.
|
||||||
|
services.AddSingleton<IBlobStorageService, FileSystemImageStorageService>();
|
||||||
|
|
||||||
|
services.AddSingleton<IImageResizer, ImageResizer>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,8 +1,14 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Databases": {
|
||||||
"LogLevel": {
|
"BaseDirectory": "data",
|
||||||
"Default": "Information",
|
"Data": {
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Provider": "sqlite",
|
||||||
|
"ConnectionString": "Data Source=data/app.db;Cache=Shared"
|
||||||
|
},
|
||||||
|
"Blob": {
|
||||||
|
"Provider": "system",
|
||||||
|
"baseUrl": "https://localhost:7273/api/photo/{id}/{res}",
|
||||||
|
"SystemContainer": "imgs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
18
back/appsettings.Production.json
Normal file
18
back/appsettings.Production.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Databases": {
|
||||||
|
"Data": {
|
||||||
|
"Provider": "sqlite",
|
||||||
|
"ConnectionString": "Data Source=data/app.db;Cache=Shared"
|
||||||
|
},
|
||||||
|
"Blob": {
|
||||||
|
"Provider": "system",
|
||||||
|
"baseUrl": "https://back.mmorales.photo/api/photo/{id}/{res}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -7,12 +7,42 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Azure.Identity" Version="1.14.2" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.8" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="9.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="9.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.8" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.8" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.8">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.UnitOfWork" Version="3.1.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.8" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
|
<PackageReference Include="Oracle.EntityFrameworkCore" Version="9.23.90" />
|
||||||
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="9.0.3" />
|
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="9.0.3" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="9.0.3" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="9.0.3" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3" />
|
||||||
|
<PackageReference Include="System.Diagnostics.EventLog" Version="9.0.8" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="9.0.8" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="persistance\data\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
52
back/context/EventContext.cs
Normal file
52
back/context/EventContext.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using back.DataModels;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace back.context;
|
||||||
|
|
||||||
|
public class EventContext : DbContext
|
||||||
|
{
|
||||||
|
private DbSet<EventModel> Events { get; set; }
|
||||||
|
public EventContext(DbContextOptions<EventContext> options)
|
||||||
|
{
|
||||||
|
Database.EnsureCreated();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<EventModel?> GetById(string id)
|
||||||
|
{
|
||||||
|
return await GetById(Guid.Parse(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<EventModel?> GetById(Guid id)
|
||||||
|
{
|
||||||
|
return await Events.FindAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetTotalItems()
|
||||||
|
{
|
||||||
|
return await Events.CountAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<EventModel>> GetPage(int page = 1, int pageSize = 20)
|
||||||
|
{
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 20;
|
||||||
|
|
||||||
|
return await Events
|
||||||
|
.OrderByDescending(p => p.CreatedAt)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Exists(EventModel? photo)
|
||||||
|
{
|
||||||
|
if (photo == null) return false;
|
||||||
|
if (string.IsNullOrEmpty(photo.Id)) return false;
|
||||||
|
return await Events.AnyAsync(p => p.Id == photo.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Exists(string id)
|
||||||
|
{
|
||||||
|
return await Events.AnyAsync(p => p.Id == id);
|
||||||
|
}
|
||||||
|
}
|
48
back/context/GalleryContext.cs
Normal file
48
back/context/GalleryContext.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using back.DataModels;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace back.context;
|
||||||
|
|
||||||
|
public class GalleryContext : DbContext
|
||||||
|
{
|
||||||
|
public DbSet<GalleryModel> Galleries { get; set; }
|
||||||
|
public GalleryContext(DbContextOptions<GalleryContext> options) : base(options)
|
||||||
|
{
|
||||||
|
// Ensure database is created
|
||||||
|
Database.EnsureCreated();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GalleryModel?> GetById(Guid id)
|
||||||
|
{
|
||||||
|
return await Galleries.FindAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetTotalItems()
|
||||||
|
{
|
||||||
|
return await Galleries.CountAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<GalleryModel>> GetPage(int page = 1, int pageSize = 20)
|
||||||
|
{
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 20;
|
||||||
|
|
||||||
|
return await Galleries
|
||||||
|
.OrderByDescending(p => p.CreatedAt)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Exists(GalleryModel? photo)
|
||||||
|
{
|
||||||
|
if (photo == null) return false;
|
||||||
|
if (string.IsNullOrEmpty(photo.Id)) return false;
|
||||||
|
return await Galleries.AnyAsync(p => p.Id == photo.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Exists(string id)
|
||||||
|
{
|
||||||
|
return await Galleries.AnyAsync(p => p.Id == id);
|
||||||
|
}
|
||||||
|
}
|
52
back/context/PersonContext.cs
Normal file
52
back/context/PersonContext.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using back.DataModels;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace back.context;
|
||||||
|
|
||||||
|
public class PersonContext : DbContext
|
||||||
|
{
|
||||||
|
private DbSet<PersonModel> Persons { get; set; }
|
||||||
|
public PersonContext(DbContextOptions<PersonContext> options)
|
||||||
|
{
|
||||||
|
Database.EnsureCreated();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PersonModel?> GetById(string id)
|
||||||
|
{
|
||||||
|
return await GetById(Guid.Parse(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PersonModel?> GetById(Guid id)
|
||||||
|
{
|
||||||
|
return await Persons.FindAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetTotalItems()
|
||||||
|
{
|
||||||
|
return await Persons.CountAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<PersonModel>> GetPage(int page = 1, int pageSize = 20)
|
||||||
|
{
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 20;
|
||||||
|
|
||||||
|
return await Persons
|
||||||
|
.OrderByDescending(p => p.CreatedAt)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Exists(PersonModel? photo)
|
||||||
|
{
|
||||||
|
if (photo == null) return false;
|
||||||
|
if (string.IsNullOrEmpty(photo.Id)) return false;
|
||||||
|
return await Persons.AnyAsync(p => p.Id == photo.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Exists(string id)
|
||||||
|
{
|
||||||
|
return await Persons.AnyAsync(p => p.Id == id);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,29 +1,235 @@
|
|||||||
using back.ApiService.models;
|
using back.DataModels;
|
||||||
|
using back.DTO;
|
||||||
|
using back.persistance.blob;
|
||||||
|
using back.services.ImageResizer;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace back.ApiService.context;
|
namespace back.context;
|
||||||
|
|
||||||
public class PhotoContext : DbContext
|
public class PhotoContext : DbContext
|
||||||
{
|
{
|
||||||
public DbSet<Photo> Photos { get; set; }
|
private DbSet<PhotoModel> Photos { get; set; }
|
||||||
|
private readonly IImageResizer _Resizer;
|
||||||
|
private readonly IBlobStorageService _BlobStorage;
|
||||||
|
|
||||||
public PhotoContext(DbContextOptions<PhotoContext> options) : base(options)
|
private readonly TagContext _tagContext;
|
||||||
|
private readonly EventContext _eventContext;
|
||||||
|
private readonly PersonContext _personContext;
|
||||||
|
|
||||||
|
public PhotoContext(DbContextOptions<PhotoContext> options,
|
||||||
|
IImageResizer resizer,
|
||||||
|
IBlobStorageService blobStorage,
|
||||||
|
TagContext tags,
|
||||||
|
EventContext events,
|
||||||
|
PersonContext persons
|
||||||
|
) : base(options)
|
||||||
{
|
{
|
||||||
// Ensure database is created
|
// Ensure database is created
|
||||||
Database.EnsureCreated();
|
Database.EnsureCreated();
|
||||||
|
_Resizer = resizer;
|
||||||
|
_BlobStorage = blobStorage;
|
||||||
|
_tagContext = tags;
|
||||||
|
_eventContext = events;
|
||||||
|
_personContext = persons;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
public async Task CreateNew(PhotoFormModel? form)
|
||||||
{
|
{
|
||||||
modelBuilder.Entity<Photo>()
|
if (form == null) { return; }
|
||||||
.Property(p => p.Tags)
|
|
||||||
.HasConversion(
|
var photo = new PhotoModel(
|
||||||
v => string.Join(',', v),
|
Guid.NewGuid().ToString(),
|
||||||
v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList());
|
form.Title,
|
||||||
modelBuilder.Entity<Photo>()
|
form.Description ?? string.Empty,
|
||||||
.Property(p => p.PersonsIn)
|
string.Empty, // LowResUrl will be set later
|
||||||
.HasConversion(
|
string.Empty, // MidResUrl will be set later
|
||||||
v => string.Join(',', v),
|
string.Empty, // HighResUrl will be set later
|
||||||
v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList());
|
DateTime.UtcNow,
|
||||||
|
DateTime.UtcNow,
|
||||||
|
form.UserId,
|
||||||
|
form.UserId
|
||||||
|
)
|
||||||
|
{
|
||||||
|
IsPublic = form.IsPublic
|
||||||
|
};
|
||||||
|
|
||||||
|
List<Task> tasks = [
|
||||||
|
SaveBlob(photo, form),
|
||||||
|
LinkTags(photo, form.Tags ?? [], form.UserId),
|
||||||
|
LinkEvent(photo, form.Evento ?? "", form.UserId),
|
||||||
|
LinkPersons(photo, form.People ?? [], form.UserId),
|
||||||
|
];
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
await Photos.AddAsync(photo);
|
||||||
|
await SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LinkPersons(PhotoModel photo, string[] personas, string updatedBy = "SYSTEM")
|
||||||
|
{
|
||||||
|
if (photo == null || personas == null || personas.Length == 0) return;
|
||||||
|
foreach (var personId in personas)
|
||||||
|
{
|
||||||
|
var person = await _personContext.GetById(personId);
|
||||||
|
if (person != null)
|
||||||
|
{
|
||||||
|
await LinkPersons(photo, person, updatedBy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LinkPersons(PhotoModel photo, PersonModel tag, string updatedBy = "SYSTEM")
|
||||||
|
{
|
||||||
|
if (tag == null) return;
|
||||||
|
// Ensure the tag exists
|
||||||
|
if (await _personContext.Exists(tag.Id))
|
||||||
|
{
|
||||||
|
photo.Persons ??= [];
|
||||||
|
photo.Persons.Add(tag);
|
||||||
|
photo.UpdatedAt = DateTime.UtcNow;
|
||||||
|
photo.UpdatedBy = updatedBy; // or use a more appropriate value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LinkTags(PhotoModel photo, string[] tags, string updatedBy = "SYSTEM")
|
||||||
|
{
|
||||||
|
if (photo == null || tags == null || tags.Length == 0) return;
|
||||||
|
foreach (var tagId in tags)
|
||||||
|
{
|
||||||
|
var tag = await _tagContext.GetById(tagId);
|
||||||
|
if (tag != null)
|
||||||
|
{
|
||||||
|
await LinkTag(photo, tag, updatedBy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LinkTag(PhotoModel photo, TagModel tag, string updatedBy = "SYSTEM")
|
||||||
|
{
|
||||||
|
if (tag == null) return;
|
||||||
|
// Ensure the tag exists
|
||||||
|
if (await _tagContext.Exists(tag.Id))
|
||||||
|
{
|
||||||
|
photo.Tags.Add(tag);
|
||||||
|
photo.UpdatedAt = DateTime.UtcNow;
|
||||||
|
photo.UpdatedBy = updatedBy; // or use a more appropriate value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LinkEvent(PhotoModel photo, string eventId, string updatedBy = "SYSTEM")
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(eventId)) return;
|
||||||
|
var evento = await _eventContext.GetById(eventId);
|
||||||
|
if (evento != null)
|
||||||
|
{
|
||||||
|
await LinkEvent(photo, evento, updatedBy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LinkEvent(PhotoModel photo, EventModel? evento, string updatedBy = "SYSTEM")
|
||||||
|
{
|
||||||
|
if (evento == null) return;
|
||||||
|
// Ensure the event exists
|
||||||
|
if (await _eventContext.Exists(evento.Id))
|
||||||
|
{
|
||||||
|
photo.Event = evento;
|
||||||
|
photo.UpdatedAt = DateTime.UtcNow;
|
||||||
|
photo.UpdatedBy = updatedBy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveBlob(PhotoModel photo, PhotoFormModel form)
|
||||||
|
{
|
||||||
|
if (form.Image != null && form.Image.Length > 0)
|
||||||
|
{
|
||||||
|
var lowRes = await _Resizer.ResizeImage(form.Image, 480);
|
||||||
|
var midRes = await _Resizer.ResizeImage(form.Image, 720);
|
||||||
|
// Upload images to blob storage
|
||||||
|
photo.Extension = form.Image.FileName.Split('.').Last();
|
||||||
|
photo.LowResUrl = $"low/{photo.Id}.webp";
|
||||||
|
photo.MidResUrl = $"mid/{photo.Id}.webp";
|
||||||
|
photo.HighResUrl = $"high/{photo.Id}.{photo.Extension}";
|
||||||
|
await _BlobStorage.SaveAsync(lowRes, photo.LowResUrl);
|
||||||
|
await _BlobStorage.SaveAsync(midRes, photo.MidResUrl);
|
||||||
|
await _BlobStorage.SaveAsync(form.Image.OpenReadStream(), photo.HighResUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PhotoModel?> GetById(string id)
|
||||||
|
{
|
||||||
|
return await GetById(Guid.Parse(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PhotoModel?> GetById(Guid id)
|
||||||
|
{
|
||||||
|
return await Photos.FindAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetTotalItems()
|
||||||
|
{
|
||||||
|
return await Photos.CountAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<PhotoModel>> GetPage(int page = 1, int pageSize = 20)
|
||||||
|
{
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 20;
|
||||||
|
|
||||||
|
return await Photos
|
||||||
|
.OrderByDescending(p => p.CreatedAt)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Exists(PhotoModel? photo)
|
||||||
|
{
|
||||||
|
if (photo == null) return false;
|
||||||
|
if (string.IsNullOrEmpty(photo.Id)) return false;
|
||||||
|
return await Photos.AnyAsync(p => p.Id == photo.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Exists(string id)
|
||||||
|
{
|
||||||
|
return await Photos.AnyAsync(p => p.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Delete(PhotoModel photo)
|
||||||
|
{
|
||||||
|
if (photo == null) return;
|
||||||
|
if (await Exists(photo))
|
||||||
|
{
|
||||||
|
// Delete the photo from blob storage
|
||||||
|
if (!string.IsNullOrEmpty(photo.LowResUrl))
|
||||||
|
await _BlobStorage.DeleteAsync(photo.LowResUrl);
|
||||||
|
if (!string.IsNullOrEmpty(photo.MidResUrl))
|
||||||
|
await _BlobStorage.DeleteAsync(photo.MidResUrl);
|
||||||
|
if (!string.IsNullOrEmpty(photo.HighResUrl))
|
||||||
|
await _BlobStorage.DeleteAsync(photo.HighResUrl);
|
||||||
|
Photos.Remove(photo);
|
||||||
|
await SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Update(PhotoModel photo)
|
||||||
|
{
|
||||||
|
if (photo == null) return;
|
||||||
|
if (await Exists(photo))
|
||||||
|
{
|
||||||
|
var evento = photo.Event;
|
||||||
|
photo.Event = null;
|
||||||
|
await LinkEvent(photo, evento, photo.UpdatedBy);
|
||||||
|
|
||||||
|
var tags = photo.Tags.Select(t => t.Id);
|
||||||
|
photo.Tags.Clear();
|
||||||
|
await LinkTags(photo, [.. tags], photo.UpdatedBy);
|
||||||
|
|
||||||
|
var persons = photo.Persons?.Select(t => t.Id) ?? [];
|
||||||
|
photo.Persons = null;
|
||||||
|
await LinkPersons(photo, [.. persons], photo.UpdatedBy);
|
||||||
|
|
||||||
|
Photos.Update(photo);
|
||||||
|
await SaveChangesAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
52
back/context/TagContext.cs
Normal file
52
back/context/TagContext.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using back.DataModels;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace back.context;
|
||||||
|
|
||||||
|
public class TagContext : DbContext
|
||||||
|
{
|
||||||
|
private DbSet<TagModel> Tags { get; set; }
|
||||||
|
public TagContext(DbContextOptions<TagContext> options)
|
||||||
|
{
|
||||||
|
Database.EnsureCreated();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TagModel?> GetById(string id)
|
||||||
|
{
|
||||||
|
return await GetById(Guid.Parse(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TagModel?> GetById(Guid id)
|
||||||
|
{
|
||||||
|
return await Tags.FindAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetTotalItems()
|
||||||
|
{
|
||||||
|
return await Tags.CountAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TagModel>> GetPage(int page = 1, int pageSize = 20)
|
||||||
|
{
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 20;
|
||||||
|
|
||||||
|
return await Tags
|
||||||
|
.OrderByDescending(p => p.CreatedAt)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Exists(TagModel? photo)
|
||||||
|
{
|
||||||
|
if (photo == null) return false;
|
||||||
|
if (string.IsNullOrEmpty(photo.Id)) return false;
|
||||||
|
return await Tags.AnyAsync(p => p.Id == photo.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Exists(string id)
|
||||||
|
{
|
||||||
|
return await Tags.AnyAsync(p => p.Id == id);
|
||||||
|
}
|
||||||
|
}
|
48
back/context/UserContext.cs
Normal file
48
back/context/UserContext.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using back.DataModels;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace back.context;
|
||||||
|
|
||||||
|
public class UserContext : DbContext
|
||||||
|
{
|
||||||
|
public DbSet<UserModel> Users { get; set; }
|
||||||
|
public UserContext(DbContextOptions<UserContext> options) : base(options)
|
||||||
|
{
|
||||||
|
// Ensure database is created
|
||||||
|
Database.EnsureCreated();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserModel?> GetById(Guid id)
|
||||||
|
{
|
||||||
|
return await Users.FindAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetTotalItems()
|
||||||
|
{
|
||||||
|
return await Users.CountAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<UserModel>> GetPage(int page = 1, int pageSize = 20)
|
||||||
|
{
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 20;
|
||||||
|
|
||||||
|
return await Users
|
||||||
|
.OrderByDescending(p => p.CreatedAt)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Exists(UserModel? photo)
|
||||||
|
{
|
||||||
|
if (photo == null) return false;
|
||||||
|
if (string.IsNullOrEmpty(photo.Id)) return false;
|
||||||
|
return await Users.AnyAsync(p => p.Id == photo.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Exists(string id)
|
||||||
|
{
|
||||||
|
return await Users.AnyAsync(p => p.Id == id);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,112 +1,98 @@
|
|||||||
using back.ApiService.context;
|
using back.context;
|
||||||
using back.ApiService.models;
|
using back.DataModels;
|
||||||
using back.DTO;
|
using back.DTO;
|
||||||
|
using back.persistance.blob;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace back.controllers;
|
namespace back.controllers;
|
||||||
|
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class PhotosController(PhotoContext photoContext) : ControllerBase
|
public class PhotosController(PhotoContext photoContext, IBlobStorageService blobStorage) : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly PhotoContext _photoContext = photoContext;
|
private readonly PhotoContext _photoContext = photoContext;
|
||||||
|
|
||||||
// GET: api/<PhotoController>
|
// GET: api/<PhotoController>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult<IEnumerable<Photo>>> Get([FromQuery] int page = 1, [FromQuery] int pageSize = 20)
|
public async Task<ActionResult<IEnumerable<PhotoModel>>> Get([FromQuery] int page = 1, [FromQuery] int pageSize = 20)
|
||||||
{
|
{
|
||||||
if (page < 1) page = 1;
|
var photos = await _photoContext.GetPage(page, pageSize);
|
||||||
if (pageSize < 1) pageSize = 20;
|
var totalItems = await _photoContext.GetTotalItems();
|
||||||
|
|
||||||
var totalItems = await _photoContext.Photos.CountAsync();
|
|
||||||
var photos = await _photoContext.Photos
|
|
||||||
.OrderByDescending(p => p.CreatedAt)
|
|
||||||
.Skip((page - 1) * pageSize)
|
|
||||||
.Take(pageSize)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
Response.Headers.Append("X-Total-Count", totalItems.ToString());
|
Response.Headers.Append("X-Total-Count", totalItems.ToString());
|
||||||
|
|
||||||
return Ok(photos);
|
return Ok(photos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET api/<PhotoController>/5
|
// GET api/<PhotoController>/5
|
||||||
[HttpGet("{id}/{res}")]
|
[HttpGet("{res}/{id}")]
|
||||||
public async Task<IActionResult> Get(Guid id, string res = "low")
|
public async Task<IActionResult> Get(string res, Guid id)
|
||||||
{
|
{
|
||||||
var photo = await _photoContext.Photos.FindAsync(id);
|
var photo = await _photoContext.GetById(id);
|
||||||
if (photo == null)
|
if (photo == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
string? filePath = res.ToLower() switch
|
string? filePath = res.ToLower() switch
|
||||||
{
|
{
|
||||||
"low" => photo.LowResUrl,
|
|
||||||
"mid" => photo.MidResUrl,
|
|
||||||
"high" => photo.HighResUrl,
|
"high" => photo.HighResUrl,
|
||||||
_ => null
|
"mid" => photo.MidResUrl,
|
||||||
|
"low" or _ => photo.LowResUrl
|
||||||
};
|
};
|
||||||
|
|
||||||
if (filePath == null || !System.IO.File.Exists(Path.Combine(Constants.Data, filePath)))
|
string? mediaType = res.ToLower() switch
|
||||||
|
{
|
||||||
|
"high" => $"image/{photo.Extension}",
|
||||||
|
"mid" or "low" or _ => "image/webp",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (filePath == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
var file = await blobStorage.GetBytesAsync(filePath);
|
||||||
|
if (file == null)
|
||||||
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
var fileBytes = await System.IO.File.ReadAllBytesAsync(Path.Combine(Constants.Data, filePath));
|
return File(file, mediaType);
|
||||||
var contentType = "image/jpeg"; // Cambia si usas otro formato
|
|
||||||
|
|
||||||
return File(fileBytes, contentType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST api/<PhotoController>
|
// POST api/<PhotoController>
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> Post([FromForm] PhotoFormModel form)
|
public async Task<IActionResult> Post([FromForm] PhotoFormModel form)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (form.Image == null || form.Image.Length == 0)
|
if (form.Image == null || form.Image.Length == 0)
|
||||||
return BadRequest("No image uploaded.");
|
return BadRequest("No image uploaded.");
|
||||||
|
|
||||||
var photo = PhotoBuilder.Build(
|
await _photoContext.CreateNew(form);
|
||||||
form.Title,
|
|
||||||
form.Description,
|
|
||||||
form.Tags?.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList(),
|
|
||||||
form.People?.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList(),
|
|
||||||
form.Image
|
|
||||||
);
|
|
||||||
|
|
||||||
// Guarda la información en la base de datos
|
|
||||||
_photoContext.Photos.Add(photo);
|
|
||||||
await _photoContext.SaveChangesAsync();
|
|
||||||
|
|
||||||
return Created();
|
return Created();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return BadRequest();
|
return BadRequest();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//// PUT api/<PhotoController>/5
|
//// PUT api/<PhotoController>
|
||||||
//[HttpPut("{id}")]
|
[HttpPut]
|
||||||
//public async Task<IActionResult> Put(Guid id, [FromBody] Photo photo)
|
public async Task<IActionResult> Put([FromBody] PhotoModel photo)
|
||||||
//{
|
{
|
||||||
// if (id != photo.Id)
|
await _photoContext.Update(photo);
|
||||||
// return BadRequest();
|
return NoContent();
|
||||||
|
}
|
||||||
// _photoContext.Entry(photo).State = EntityState.Modified;
|
|
||||||
// await _photoContext.SaveChangesAsync();
|
|
||||||
// return NoContent();
|
|
||||||
//}
|
|
||||||
|
|
||||||
// DELETE api/<PhotoController>/5
|
// DELETE api/<PhotoController>/5
|
||||||
[HttpDelete("{id}")]
|
[HttpDelete("{id}")]
|
||||||
public async Task<IActionResult> Delete(Guid id)
|
public async Task<IActionResult> Delete(Guid id)
|
||||||
{
|
{
|
||||||
var photo = await _photoContext.Photos.FindAsync(id);
|
var photo = await _photoContext.GetById(id);
|
||||||
if (photo == null)
|
if (photo == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
_photoContext.Photos.Remove(photo);
|
await _photoContext.Delete(photo);
|
||||||
await _photoContext.SaveChangesAsync();
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,96 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using SixLabors.ImageSharp;
|
|
||||||
using SixLabors.ImageSharp.Processing;
|
|
||||||
|
|
||||||
namespace back.ApiService.models;
|
|
||||||
|
|
||||||
public class PhotoBuilder
|
|
||||||
{
|
|
||||||
public static Photo Build(string? title, string? description, List<string>? tags, List<string>? personsIn, IFormFile image)
|
|
||||||
{
|
|
||||||
// Genera un nombre de archivo único
|
|
||||||
var id = Guid.NewGuid();
|
|
||||||
var fileName = $"{id}{Path.GetExtension(image.FileName)}";
|
|
||||||
var photo = new Photo(title, description, tags, personsIn, fileName)
|
|
||||||
{
|
|
||||||
Id = id
|
|
||||||
};
|
|
||||||
|
|
||||||
// Asegura que los directorios existen
|
|
||||||
Directory.CreateDirectory(Path.Join(Constants.Data, Photo.LowResFolder));
|
|
||||||
Directory.CreateDirectory(Path.Join(Constants.Data, Photo.MidResFolder));
|
|
||||||
Directory.CreateDirectory(Path.Join(Constants.Data, Photo.HighResFolder));
|
|
||||||
|
|
||||||
// Procesa y guarda las imágenes
|
|
||||||
using var stream = image.OpenReadStream();
|
|
||||||
using var img = Image.Load(stream);
|
|
||||||
// Baja resolución (480px)
|
|
||||||
img.Mutate(x => x.Resize(new ResizeOptions { Size = new Size(480, 0), Mode = ResizeMode.Max }));
|
|
||||||
img.Save(Path.Join(Constants.Data, photo.LowResUrl));
|
|
||||||
|
|
||||||
// Media resolución (720px)
|
|
||||||
img.Mutate(x => x.Resize(new ResizeOptions { Size = new Size(720, 0), Mode = ResizeMode.Max }));
|
|
||||||
img.Save(Path.Join(Constants.Data, photo.MidResUrl));
|
|
||||||
|
|
||||||
// Original
|
|
||||||
stream.Position = 0;
|
|
||||||
using var original = Image.Load(stream);
|
|
||||||
original.Save(Path.Join(Constants.Data, photo.HighResUrl));
|
|
||||||
|
|
||||||
return photo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Table("Photo")]
|
|
||||||
public class Photo
|
|
||||||
{
|
|
||||||
public const string LowResFolder = "imgs/low";
|
|
||||||
public const string MidResFolder = "imgs/mid";
|
|
||||||
public const string HighResFolder = "imgs/high";
|
|
||||||
|
|
||||||
[Key]
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
public string? Title { get; set; }
|
|
||||||
public string? Description { get; set; }
|
|
||||||
public string? Ubicación { get; set; }
|
|
||||||
public string? Evento { get; set; }
|
|
||||||
public List<string>? Tags { get; set; }
|
|
||||||
public List<string>? PersonsIn { get; set; }
|
|
||||||
public string? FileName { get; set; }
|
|
||||||
public string LowResUrl { get => Path.Join(LowResFolder, FileName); }
|
|
||||||
public string MidResUrl { get => Path.Join(MidResFolder, FileName); }
|
|
||||||
public string HighResUrl { get => Path.Join(HighResFolder, FileName); }
|
|
||||||
public float? Ranking { get; set; }
|
|
||||||
public DateTime? CreatedAt { get; set; }
|
|
||||||
public DateTime? UpdatedAt { get; set; }
|
|
||||||
public string? CreatedBy { get; set; }
|
|
||||||
public string? UpdatedBy { get; set; }
|
|
||||||
|
|
||||||
private Photo()
|
|
||||||
{
|
|
||||||
//Id = Guid.NewGuid();
|
|
||||||
Tags = [];
|
|
||||||
PersonsIn = [];
|
|
||||||
CreatedAt = DateTime.Now;
|
|
||||||
UpdatedAt = DateTime.Now;
|
|
||||||
CreatedBy = "system";
|
|
||||||
UpdatedBy = "system";
|
|
||||||
Ranking = 0.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Photo(string? title, string? description, List<string>? tags, List<string>? personsIn, string? fileName)
|
|
||||||
: this()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(fileName))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("FileName cannot be null or empty.", nameof(fileName));
|
|
||||||
}
|
|
||||||
Title = title;
|
|
||||||
Description = description;
|
|
||||||
Tags = tags ?? [];
|
|
||||||
PersonsIn = personsIn ?? [];
|
|
||||||
FileName = fileName;
|
|
||||||
}
|
|
||||||
}
|
|
104
back/persistance/blob/FileSystemImageStorageService.cs
Normal file
104
back/persistance/blob/FileSystemImageStorageService.cs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
using back.Options;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace back.persistance.blob;
|
||||||
|
|
||||||
|
public class FileSystemImageStorageService(
|
||||||
|
IOptions<Databases> systemOptions,
|
||||||
|
IOptionsMonitor<DatabaseConfig> options,
|
||||||
|
IMemoryCache memoryCache
|
||||||
|
) : IBlobStorageService
|
||||||
|
{
|
||||||
|
private readonly string RootPath = systemOptions.Value.BaseDirectory ?? "";
|
||||||
|
private readonly DatabaseConfig config = options.Get(DatabaseConfig.BlobStorage);
|
||||||
|
private readonly IMemoryCache cache = memoryCache;
|
||||||
|
|
||||||
|
private string GetFullPath(string fileName)
|
||||||
|
{
|
||||||
|
// Ensure the directory exists
|
||||||
|
var directory = Path.Join(RootPath, config.SystemContainer, fileName);
|
||||||
|
if (directory != null && !Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(string fileName)
|
||||||
|
{
|
||||||
|
var path = GetFullPath(fileName);
|
||||||
|
if (cache.TryGetValue(path, out Stream cachedStream))
|
||||||
|
{
|
||||||
|
cachedStream.Dispose(); // Dispose the cached stream if it exists
|
||||||
|
cache.Remove(path); // Remove from cache
|
||||||
|
}
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return; // No file to delete
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Error deleting file {fileName}: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Stream?> GetStreamAsync(string fileName)
|
||||||
|
{
|
||||||
|
var path = GetFullPath(fileName);
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
if (cache.TryGetValue(path, out Stream? cachedStream))
|
||||||
|
{
|
||||||
|
return Task.FromResult<Stream?>(cachedStream);
|
||||||
|
}
|
||||||
|
// open the file stream for multiple reads and cache it for performance
|
||||||
|
var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
|
||||||
|
cache.CreateEntry(path)
|
||||||
|
.SetValue(fileStream)
|
||||||
|
.SetSlidingExpiration(TimeSpan.FromMinutes(30)); // Cache for 30 minutes
|
||||||
|
|
||||||
|
return Task.FromResult<Stream?>(fileStream);
|
||||||
|
}
|
||||||
|
return Task.FromResult<Stream?>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<byte[]?> GetBytesAsync(string fileName)
|
||||||
|
{
|
||||||
|
var stream = await GetStreamAsync(fileName);
|
||||||
|
if (stream != null)
|
||||||
|
{
|
||||||
|
using var memoryStream = new MemoryStream();
|
||||||
|
await stream.CopyToAsync(memoryStream);
|
||||||
|
return memoryStream.ToArray();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveAsync(Stream blobStream, string fileName)
|
||||||
|
{
|
||||||
|
var path = GetFullPath(fileName);
|
||||||
|
if (cache.TryGetValue(path, out Stream? _) || File.Exists(path))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"File {fileName} already exists. Use Update for updating file info.");
|
||||||
|
}
|
||||||
|
using var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write);
|
||||||
|
await blobStream.CopyToAsync(fileStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(Stream blobStream, string fileName)
|
||||||
|
{
|
||||||
|
var path = GetFullPath(fileName);
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
await DeleteAsync(fileName);
|
||||||
|
}
|
||||||
|
await SaveAsync(blobStream, fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
11
back/persistance/blob/IBlobStorageService.cs
Normal file
11
back/persistance/blob/IBlobStorageService.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace back.persistance.blob;
|
||||||
|
|
||||||
|
public interface IBlobStorageService
|
||||||
|
{
|
||||||
|
Task SaveAsync(Stream blobStream, string fileName);
|
||||||
|
Task<Stream?> GetStreamAsync(string fileName);
|
||||||
|
Task<byte[]?> GetBytesAsync(string fileName);
|
||||||
|
Task DeleteAsync(string fileName);
|
||||||
|
Task UpdateAsync(Stream blobStream, string fileName);
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
6
back/services/ImageResizer/IImageResizer.cs
Normal file
6
back/services/ImageResizer/IImageResizer.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace back.services.ImageResizer;
|
||||||
|
|
||||||
|
public interface IImageResizer
|
||||||
|
{
|
||||||
|
Task<Stream> ResizeImage(IFormFile image, int v);
|
||||||
|
}
|
23
back/services/ImageResizer/ImageResizer.cs
Normal file
23
back/services/ImageResizer/ImageResizer.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
|
||||||
|
namespace back.services.ImageResizer;
|
||||||
|
|
||||||
|
public sealed class ImageResizer : IImageResizer
|
||||||
|
{
|
||||||
|
public async Task<Stream> ResizeImage(IFormFile image, int maxRes)
|
||||||
|
{
|
||||||
|
if (image == null || image.Length == 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Invalid image file.");
|
||||||
|
}
|
||||||
|
using var inputStream = image.OpenReadStream();
|
||||||
|
using var outputStream = new MemoryStream();
|
||||||
|
using var img = Image.Load(inputStream);
|
||||||
|
|
||||||
|
img.Mutate(x => x.Resize(new ResizeOptions { Size = new Size(maxRes, 0), Mode = ResizeMode.Max }));
|
||||||
|
await img.SaveAsWebpAsync(outputStream);
|
||||||
|
outputStream.Position = 0;
|
||||||
|
return outputStream;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,2 @@
|
|||||||
|
nvs latest
|
||||||
|
node .\_fix-svg-stroke.js
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "config.schema.json",
|
"$schema": "config.schema.json",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"baseApiUrl": "https://back.mmorales.photo/api",
|
"baseApiUrl": "https://localhost:7273/api",
|
||||||
"defaultTheme": "light",
|
"defaultTheme": "light",
|
||||||
"logoAlt": "MMORALES PHOTO",
|
"logoAlt": "MMORALES PHOTO",
|
||||||
"title": "MMORALES PHOTO",
|
"title": "MMORALES PHOTO",
|
||||||
|
@@ -11,6 +11,7 @@ import { EventsView } from './views/events-view/events-view';
|
|||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ path: '', component: HomeView },
|
{ path: '', component: HomeView },
|
||||||
|
{ path: 'home', component: HomeView },
|
||||||
{ path: 'login', component: LoginView },
|
{ path: 'login', component: LoginView },
|
||||||
{ path: 'tags', component: TagsView },
|
{ path: 'tags', component: TagsView },
|
||||||
{ path: 'events', component: EventsView },
|
{ path: 'events', component: EventsView },
|
||||||
|
@@ -1,14 +1,12 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { RouterOutlet } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
import { Header } from './global-components/header/header';
|
import { Header } from './global-components/header/header';
|
||||||
import { HomeView } from './views/home-view/home-view';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [Header, HomeView, RouterOutlet],
|
imports: [Header, RouterOutlet],
|
||||||
templateUrl: './app.html',
|
templateUrl: './app.html',
|
||||||
styleUrl: './app.scss',
|
styleUrl: './app.scss',
|
||||||
})
|
})
|
||||||
export class App {
|
export class App {
|
||||||
protected isHomeView = true;
|
|
||||||
}
|
}
|
||||||
|
@@ -11,6 +11,7 @@ header {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
logo {
|
logo {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<ul>
|
<ul>
|
||||||
@if (gallery.photos) { @for (item of gallery.photos; track $index) {
|
@if (gallery().photos) { @for (item of gallery().photos; track $index) {
|
||||||
<li>
|
<li>
|
||||||
<img [src]="item.lowResUrl" [alt]="item.title" />
|
<img [src]="item.lowResUrl" [alt]="item.title" />
|
||||||
</li>
|
</li>
|
||||||
|
@@ -0,0 +1,49 @@
|
|||||||
|
$gap-size: 20px;
|
||||||
|
|
||||||
|
.low-res-image-list {
|
||||||
|
columns: 4;
|
||||||
|
column-gap: $gap-size;
|
||||||
|
list-style: none;
|
||||||
|
margin-top: 3%;
|
||||||
|
min-height: 100lvh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile - 1 column */
|
||||||
|
@media (max-width: 639px) {
|
||||||
|
.low-res-image-list {
|
||||||
|
columns: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablets in portrait - 3 columns for better balance */
|
||||||
|
@media (min-width: 640px) and (max-width: 1023px) and (orientation: portrait) {
|
||||||
|
.low-res-image-list {
|
||||||
|
columns: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablets in landscape - 1 column */
|
||||||
|
@media (min-width: 640px) and (max-width: 1023px) and (orientation: landscape) {
|
||||||
|
.low-res-image-list {
|
||||||
|
columns: 1;
|
||||||
|
max-width: 90%;
|
||||||
|
min-height: 95vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop - mosaic with margins */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.low-res-image-list {
|
||||||
|
columns: 4;
|
||||||
|
margin-left: 17svw;
|
||||||
|
margin-right: 17svw;
|
||||||
|
max-width: calc(100vw - 34svw);
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit, signal } from '@angular/core';
|
||||||
import { homeGallery } from '../../../models/gallery/homeGallery';
|
import { homeGallery } from '../../../models/gallery/homeGallery';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -8,7 +8,7 @@ import { homeGallery } from '../../../models/gallery/homeGallery';
|
|||||||
styleUrl: './home-view.scss',
|
styleUrl: './home-view.scss',
|
||||||
})
|
})
|
||||||
export class HomeView implements OnInit {
|
export class HomeView implements OnInit {
|
||||||
public gallery: homeGallery = new homeGallery();
|
public readonly gallery = signal<homeGallery>(new homeGallery());
|
||||||
|
|
||||||
ngOnInit() {}
|
ngOnInit() {}
|
||||||
}
|
}
|
||||||
|
@@ -26,15 +26,30 @@ export class paginator<T> {
|
|||||||
private params: Record<string, any> = {};
|
private params: Record<string, any> = {};
|
||||||
|
|
||||||
private page: number = 1;
|
private page: number = 1;
|
||||||
private totalItems: number = 0;
|
private totalItems: number = 20;
|
||||||
private isLoading: boolean = false;
|
private isLoading: boolean = false;
|
||||||
|
private alreadyLoadedPages: Record<number, T[]> = {};
|
||||||
|
|
||||||
get totalPages(): number {
|
get totalPages(): number {
|
||||||
return Math.ceil(this.totalItems / this.maxItemPerPage);
|
return Math.ceil(this.totalItems / this.maxItemPerPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasNextPage(): boolean {
|
get hasNextPage(): boolean {
|
||||||
return this.page < this.totalPages;
|
return this.page <= this.totalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPage(number: number): Promise<T[] | null> {
|
||||||
|
if (number < 1) {
|
||||||
|
return this.alreadyLoadedPages[1] || null;
|
||||||
|
}
|
||||||
|
if (number > this.totalPages) {
|
||||||
|
if (this.hasNextPage) {
|
||||||
|
this.loadNextPage();
|
||||||
|
return this.alreadyLoadedPages[number] || null;
|
||||||
|
}
|
||||||
|
return this.alreadyLoadedPages[this.totalPages] || null;
|
||||||
|
}
|
||||||
|
return this.alreadyLoadedPages[number] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async loadNextPage(): Promise<T[] | null> {
|
public async loadNextPage(): Promise<T[] | null> {
|
||||||
@@ -54,8 +69,10 @@ export class paginator<T> {
|
|||||||
this.totalItems = response.headers['x-total-count']
|
this.totalItems = response.headers['x-total-count']
|
||||||
? parseInt(response.headers['x-total-count'], 10)
|
? parseInt(response.headers['x-total-count'], 10)
|
||||||
: 0;
|
: 0;
|
||||||
|
const data = response.data as T[];
|
||||||
|
this.alreadyLoadedPages[this.page] = data;
|
||||||
this.page++;
|
this.page++;
|
||||||
return response.data as T[];
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading photos:', error);
|
console.error('Error loading photos:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
Reference in New Issue
Block a user