Files
raven/server/AyaNova/biz/AttachmentBiz.cs
2023-03-22 00:02:46 +00:00

261 lines
13 KiB
C#

using System.Threading.Tasks;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Models;
using Microsoft.Extensions.Logging;
using System;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Collections.Generic;
namespace AyaNova.Biz
{
/// <summary>
/// Handle attachment file related cleanup and checking
/// </summary>
internal class AttachmentBiz : BizObject, IJobObject
{
internal AttachmentBiz(AyContext dbcontext, long currentUserId, AuthorizationRoles userRoles)
{
ct = dbcontext;
UserId = currentUserId;
CurrentUserRoles = userRoles;
BizType = AyaType.FileAttachment;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//JOB / OPERATIONS
//
public async Task HandleJobAsync(OpsJob job)
{
//Hand off the particular job to the corresponding processing code
//NOTE: If this code throws an exception the caller (JobsBiz::ProcessJobsAsync) will automatically set the job to failed and log the exeption so
//basically any error condition during job processing should throw up an exception if it can't be handled
//There might be future other job types so doing it like this for all biz job handlers for now
switch (job.JobType)
{
case JobType.AttachmentMaintenance:
await ProcessAttachmentMaintenanceAsync(job);
break;
default:
throw new System.ArgumentOutOfRangeException($"AttachmentBiz.HandleJobAsync -> Invalid job type{job.JobType.ToString()}");
}
}
/// <summary>
/// Handle the job
/// </summary>
/// <param name="job"></param>
private async Task ProcessAttachmentMaintenanceAsync(OpsJob job)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger("AttachmentMaintenanceJob");
ApiServerState apiServerState = (ApiServerState)ServiceProviderProvider.Provider.GetService(typeof(ApiServerState));
//get the current server state so can set back to it later
ApiServerState.ServerState wasServerState = apiServerState.GetState();
string wasReason = apiServerState.Reason;
try
{
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running);
await JobsBiz.LogJobAsync(job.GId, $"LT:StartJob ");
apiServerState.SetOpsOnly("Attachment file maintenance");
//get a list of all attachment files currently on disk
var AllAttachmentFilesOnDisk = FileUtil.GetAllAttachmentFilePaths();
List<string> AllDBFileFullPath = new List<string>();
// EXISTENCE CHECK
//iterate all records in chunks, update the existence bool field if it's incorrect only
bool moreRecords = true;
int skip = 0;
int chunkSize = 100;
do
{
var chunk = await ct.FileAttachment.AsNoTracking().OrderBy(z => z.Id).Skip(skip).Take(chunkSize).ToListAsync();
if (chunk.Count < chunkSize)
{
//we've reached the end
moreRecords = false;
}
skip += chunkSize;
foreach (var i in chunk)
{
var FullPathName = FileUtil.GetPermanentAttachmentFilePath(i.StoredFileName);
AllDBFileFullPath.Add(FullPathName);
var FileExistsInReality = AllAttachmentFilesOnDisk.Contains(FullPathName);
var ParentBizObjectExistsInReality = await BizObjectExistsInDatabase.ExistsAsync(i.AttachToAType, i.AttachToObjectId, ct);
//does the db record reflect the same status as reality?
if (FileExistsInReality != i.Exists || !ParentBizObjectExistsInReality)
{
var f = await ct.FileAttachment.FirstOrDefaultAsync(z => z.Id == i.Id);
if (f != null)
{
f.Exists = FileExistsInReality;
if (!ParentBizObjectExistsInReality)
{
//switch it to notype
f.AttachToAType = AyaType.NoType;
f.AttachToObjectId = 0;
}
await ct.SaveChangesAsync();
}
}
//DOES THE PARENT OBJECT EXIST?
}
} while (moreRecords);
//I kept this block because I did a lot of work to figure it out but in the end I don't need it because
//a user will be moving attachments so they would no longer be existing on their old NOTYPE orphan location anyway
// //DE-ORPHANIZE ACTION (clean up former orphans)
// //people can attach an orphan to another record so this cleans that up
// //also, potentiallly
// //iterate orphaned file attachments to NOTHING type, if found to be attached to any other object remove the orphaned object attachment record in db
// //but keeping the physical file since it's attached to something else
// var AllOrphansInDb = await ct.FileAttachment.Where(z => z.AttachToAType == AyaType.NoType).ToListAsync();
// foreach (FileAttachment OrphanInDb in AllOrphansInDb)
// {
// if (await ct.FileAttachment.AnyAsync(z => z.StoredFileName==OrphanInDb.StoredFileName && z.AttachToAType != AyaType.NoType))
// {
// //It is also attached to something else so remove it from the nothing type
// ct.FileAttachment.Remove(OrphanInDb);
// await ct.SaveChangesAsync();
// }
// }
// ORPHANED FILES CHECK
var FilesOnDiskNotInDb = AllAttachmentFilesOnDisk.Except(AllDBFileFullPath);
if (FilesOnDiskNotInDb.Count() > 0)
await JobsBiz.LogJobAsync(job.GId, $"Found {FilesOnDiskNotInDb.Count()} physical files not known to be existing Attachments, creating attachment records tied to 'NoType' so they show in UI");
// FOREIGN FILES placed in folder directly outside of attachment system
// user thinks they can just drop them in or accidentally copies them here
// Or, user renames a folder for some reason?
// This is a good reason not to delete them, because they can just un-rename them to fix it
// SWEEPER JOB
// I think it should delete them outright, but maybe that's a bad idea, not sure
// ID them to see if they *should* be one of ours by the file name I guess since it's the only identifying characteristic
// is it in the correct folder which is named based on it's hash?
// Is it X characters long (they all are or not?)
// Does it have an extension? None of ours have an extension
//Vet the file and see if it has the characteristics of an attachment before re-attaching it, if not, compile into a list then log it and notify
//Attach any found into the NOTHING object type with id 0 so they will be represented in attachment list for being dealt with
List<string> ForeignFilesNotLikelyAttachmentsFoundInAttachmentsFolder = new List<string>();
foreach (string orphan in FilesOnDiskNotInDb)
{
if (FileUtil.AppearsToBeAnOrphanedAttachment(orphan))
{
FileAttachment fa = new FileAttachment();
fa.AttachToObjectId = 0;
fa.AttachToAType = AyaType.NoType;
fa.ContentType = "application/octet-stream";//most generic type, we don't know what it is
fa.DisplayFileName = "FOUND" + FileUtil.GetSafeDateFileName();
fa.LastModified = DateTime.UtcNow;
fa.Notes = "Found in attachments folder not linked to an object";
fa.StoredFileName = System.IO.Path.GetFileName(orphan);
await ct.FileAttachment.AddAsync(fa);
await ct.SaveChangesAsync();
}
else
{
log.LogDebug($"Foreign file found in attachments folder but doesn't appear to belong {orphan}");
ForeignFilesNotLikelyAttachmentsFoundInAttachmentsFolder.Add(orphan);
}
}
//ok, these files could be important and shouldn't be here so notify and log as much as possible
var ffcount = ForeignFilesNotLikelyAttachmentsFoundInAttachmentsFolder.Count;
if (ffcount > 0)
{
string msg = string.Empty;
if (ffcount < 25)
{
//we can list them all
msg = $"{ffcount} files found in attachments folder that don't appear to belong there:";
}
else
{
msg = $"{ffcount} files found in attachments folder that don't appear to belong there, here are the first 25";
}
log.LogDebug(msg);
await JobsBiz.LogJobAsync(job.GId, msg);
await NotifyEventHelper.AddOpsProblemEvent($"Attachments issue:{msg}");
var outList = ForeignFilesNotLikelyAttachmentsFoundInAttachmentsFolder.Take(25).ToList();
foreach (string s in outList)
{
log.LogDebug(s);
await JobsBiz.LogJobAsync(job.GId, s);
}
}
await JobsBiz.LogJobAsync(job.GId, "Finished.");
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed);
}
catch (Exception ex)
{
log.LogError(ex, "AttachmentMaintenanceJob error during ops");
await JobsBiz.LogJobAsync(job.GId, $"AttachmentMaintenanceJob error during ops\r\n{ex.Message}");
throw;
}
finally
{
log.LogInformation($"AttachmentMaintenanceJob: setting server state back to {wasServerState.ToString()}");
apiServerState.SetState(wasServerState, wasReason);
}
}
//Other job handlers here...
////////////////////////////////////////////////////////////////////////////////////////////////
//DUPLICATE ATTACHMENTS TO NEW OBJECT
//
internal static async Task DuplicateAttachments(AyaTypeId aSource, AyaTypeId aDest, long attachedByUserId, AyContext ct)
{
var sources = await ct.FileAttachment.AsNoTracking()
.Where(z => z.AttachToAType == aSource.AType && z.AttachToObjectId == aSource.ObjectId)
.ToListAsync();
if (sources.Count > 0)
{
foreach (var src in sources)
{
ct.FileAttachment.Add(new FileAttachment
{
AttachToObjectId = aDest.ObjectId,
AttachToAType = aDest.AType,
StoredFileName = src.StoredFileName,
DisplayFileName = src.DisplayFileName,
ContentType = src.ContentType,
LastModified = src.LastModified,
Notes = src.Notes,
Exists = src.Exists,
Size = src.Size,
AttachedByUserId=attachedByUserId//
});
}
await ct.SaveChangesAsync();
}
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons