261 lines
13 KiB
C#
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
|
|
|