using System; using System.IO; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Security.Cryptography.X509Certificates; namespace DataOrganizer { class Snapshot { public Folder Root { get; private set; } public Dictionary Files { get; private set; } = new Dictionary(); public Dictionary Directories { get; private set; } = new Dictionary(); public Dictionary FileDuplicates { get; private set; } = new Dictionary(); //Contains files associated with particular, first level folders. //So if the root folder group is X:\Anime and we have a folder path of x:\Anime\Death Note, then indexing this dictonary //with the text "Death Note" would return all files in that folder. public Dictionary> TopLevelFolders { get; private set; } = new Dictionary>(); public Snapshot(List fileRecords, DB.FolderGroup folderGroup) { Root = new Folder(folderGroup.Name, folderGroup.Path, null); Directories.Add(Root.FullPath, Root); if (folderGroup.Type == DB.OrganizationType.SingleLevel) ProcessFilesAsOneLevel(fileRecords, folderGroup); //else if (folderGroup.Type == DB.OrganizationType.Flat) // ProcessFilesAsFlat(fileRecords, folderGroup); } public Snapshot(List files, DB.FolderGroup folderGroup) { Root = new Folder(folderGroup.Name, folderGroup.Path, null); Directories.Add(Root.FullPath, Root); if (folderGroup.Type == DB.OrganizationType.SingleLevel) ProcessFilesAsOneLevel(files, folderGroup); } private void ProcessFilesAsOneLevel(List files, DB.FolderGroup folderGroup) { Folder firstLevelFolder = null; foreach (var file in files) { var folders = file.PathNoRoot.Split(new char[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); var orgFolder = Path.Combine(folderGroup.Path, folders[0]); var currentPath = Path.Combine(folderGroup.Path, folders[0]); if(firstLevelFolder == null || firstLevelFolder.Name != folders[0]) { firstLevelFolder = new Folder(folders[0], Path.Combine(folderGroup.Path, folders[0]), Root); Directories.Add(orgFolder, firstLevelFolder); TopLevelFolders.Add(orgFolder, new List()); Root.AddDirectory(firstLevelFolder); } for(var i = 1; i < folders.Length - 1; i++) { var parentFolder = currentPath; currentPath = Path.Combine(currentPath, folders[i]); if(!Directories.ContainsKey(currentPath)) { var newFolder = new Folder(folders[i], currentPath, Directories[parentFolder]); Directories.Add(currentPath, newFolder); Directories[parentFolder].AddDirectory(newFolder); } } //var duplicates = OrganizationFolders[orgFolder].Where(x => x.Size == file.Size && x.Id != file.Id); //foreach(var dup in duplicates) //{ // if(FileDuplicates.ContainsKey(file.PathNoRoot)) // { // FileDuplicates[file.PathNoRoot].Duplicates.Add(dup); // } // else FileDuplicates.Add(file.PathNoRoot, new Duplicate() { File = file, Duplicates = new List() { dup } }); //} var snap = new FileSnapshot(file, firstLevelFolder, Directories[currentPath], folderGroup.Path); TopLevelFolders[orgFolder].Add(snap); Directories[currentPath].AddFile(snap); Files.Add(Path.Combine(folderGroup.Path, file.PathNoRoot), snap); } } private void ProcessFilesAsOneLevel(List files, DB.FolderGroup folderGroup) { Folder firstLevelFolder = null; foreach (var file in files) { //Add one to the length to remove the trailing slash in the root path. var pathNoRoot = file.FullName.Remove(0, folderGroup.Path.Length + 1); var folders = pathNoRoot.Split(new char[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); var orgFolder = Path.Combine(folderGroup.Path, folders[0]); var currentPath = Path.Combine(folderGroup.Path, folders[0]); if (firstLevelFolder == null || firstLevelFolder.Name != folders[0]) { firstLevelFolder = new Folder(folders[0], Path.Combine(folderGroup.Path, folders[0]), Root); Directories.Add(orgFolder, firstLevelFolder); TopLevelFolders.Add(orgFolder, new List()); Root.AddDirectory(firstLevelFolder); } for (var i = 1; i < folders.Length - 1; i++) { var parentFolder = currentPath; currentPath = Path.Combine(currentPath, folders[i]); if (!Directories.ContainsKey(currentPath)) { var newFolder = new Folder(folders[i], currentPath, Directories[parentFolder]); Directories.Add(currentPath, newFolder); Directories[parentFolder].AddDirectory(newFolder); } } //var duplicates = OrganizationFolders[orgFolder].Where(x => x.Size == file.Size && x.Id != file.Id); //foreach(var dup in duplicates) //{ // if(FileDuplicates.ContainsKey(file.PathNoRoot)) // { // FileDuplicates[file.PathNoRoot].Duplicates.Add(dup); // } // else FileDuplicates.Add(file.PathNoRoot, new Duplicate() { File = file, Duplicates = new List() { dup } }); //} var snap = new FileSnapshot(file, firstLevelFolder, Directories[currentPath], folderGroup.Path); TopLevelFolders[orgFolder].Add(snap); Directories[currentPath].AddFile(snap); Files.Add(snap.FullPath, snap); } } private void ProcessFilesAsFlat(List files, DB.FolderGroup folderGroup) { } public void DeleteFile(FileSnapshot file) { if (Files.ContainsKey(file.FullPath)) Files.Remove(file.FullPath); Root.DeleteFile(file); file.ParentFolder.DeleteFile(file); } public void DeleteFolder(Folder folder) { ClearSubFolders(folder); } private void ClearSubFolders(Folder folder) { if (Directories.ContainsKey(folder.FullPath)) Directories.Remove(folder.FullPath); if (TopLevelFolders.ContainsKey(folder.FullPath)) TopLevelFolders.Remove(folder.FullPath); Root.DeleteFolder(folder); folder.Parent.DeleteFolder(folder); foreach (var dir in folder.Folders) ClearSubFolders(dir.Value); } public void MergeSnapshot(Snapshot snapshot) { var newFiles = new List(); var existingFiles = Files.Values.Except(snapshot.Files.Values); foreach(var file in snapshot.Files) if (!Files.ContainsKey(file.Key)) newFiles.Add(file.Value); foreach (var folder in snapshot.TopLevelFolders) { if (!TopLevelFolders.ContainsKey(folder.Key) && folder.Key != Root.FullPath) { Root.AddDirectory(snapshot.Directories[folder.Key]); TopLevelFolders.Add(folder.Key, new List()); } } foreach (var existingFile in existingFiles) { if (!snapshot.Files.ContainsKey(existingFile.FullPath)) { existingFile.Status = FileSnapshot.FileStatus.PendingDelete; } else if (existingFile.ModifiedDate != snapshot.Files[existingFile.FullPath].ModifiedDate) { existingFile.ModifiedDate = snapshot.Files[existingFile.FullPath].ModifiedDate; } } foreach(var newFile in newFiles) { Files.Add(newFile.FullPath, newFile); Folder folder = newFile.ParentFolder; while(folder != null) { if (!Directories.ContainsKey(folder.FullPath)) Directories.Add(folder.FullPath, folder); folder = folder.Parent; } TopLevelFolders[newFile.TopLevelFolder.FullPath].Add(newFile); } } public SnapshotComparsion Diff(Snapshot snapshot) { return Compare(this, snapshot); } private SnapshotComparsion Compare(Snapshot a, Snapshot b) { var newFiles = new List(); var modifiedFiles = new List(); var deletedFiles = new List(); var existingFiles = a.Files.Values.Except(b.Files.Values); var newOrgFolders = new List(); foreach (var file in b.Files) if (!a.Files.ContainsKey(file.Key)) newFiles.Add(file.Value); foreach (var folder in b.TopLevelFolders) { if (!a.TopLevelFolders.ContainsKey(folder.Key) && folder.Key != a.Root.FullPath) { newOrgFolders.Add(b.Directories[folder.Key]); } } foreach (var existingFile in existingFiles) { if (!b.Files.ContainsKey(existingFile.FullPath)) { deletedFiles.Add(existingFile); } else if (existingFile.ModifiedDate != b.Files[existingFile.FullPath].ModifiedDate) { modifiedFiles.Add(b.Files[existingFile.FullPath]); } } return new SnapshotComparsion() { NewFiles = newFiles, ModifiedFiles = modifiedFiles, DeletedFiles = deletedFiles, NewTopLevelFolders = newOrgFolders }; } public static List FindFileRenames(Folder a, Folder b) { var renames = new List(); foreach(var file in a.Files.Values) { foreach(var f in b.Files.Values) { if (file.Size == f.Size && file.CreatedDate == f.CreatedDate && file.ModifiedDate == f.ModifiedDate) { if(f.Name != file.Name) renames.Add(new SnapshotComparsion.RenamedFile() { From = file, To = f }); } } } return renames; } public static List FindFileMoves(Folder a, Folder b) { var files = new List(); var filesA = a.GetAllFiles(); var filesB = b.GetAllFiles(); foreach(var fileA in filesA) { foreach(var fileB in filesB) { if(fileB.CreatedDate == fileA.CreatedDate && fileA.ModifiedDate == fileB.ModifiedDate && fileA.Size == fileB.Size) { files.Add(new SnapshotComparsion.RenamedFile { From = fileA, To = fileB }); continue; } } } return files; } public static List FindDuplicates(Folder topLevelFolder) { var duplicates = new Dictionary(); var files = topLevelFolder.GetAllFiles(); var fileCount = 0; var seasonFolderName = new Regex(@"^(?i)season\W+\d+"); var extrasFolderName = new Regex(@"^(?i)(?:extras|specials|movies)+$"); fileCount = files.Count; for(var i = 0; i < fileCount; i++) { var file = files[i]; //if (!duplicates.ContainsKey(file.FullPath)) duplicates.Add(file.FullPath, new Duplicate() { File = file, Duplicates = new List() }); var dups = files.Where(x => x.Size == file.Size && x.FileId != file.FileId).ToList(); var dupCount = dups.Count; if(dupCount > 0) if (!duplicates.ContainsKey(file.FullPath)) duplicates.Add(file.FullPath, new Duplicate() { File = file, Duplicates = new List() }); for (var j = 0; j < dupCount; j++) { var dup = dups[j]; files.Remove(dup); duplicates[file.FullPath].Duplicates.Add(dup); fileCount--; } } return duplicates.Values.ToList(); } public struct Duplicate { public FileSnapshot File; public List Duplicates; } } public class Folder { public string Name { get; private set; } public string FullPath { get; private set; } public Folder Parent { get; private set; } public Dictionary Files { get; private set; } = new Dictionary(); public Dictionary Folders { get; private set; } = new Dictionary(); public bool IsEmpty { get { return !(Files.Count > 0 || Folders.Count > 0); } } public bool IsRootFolder { get { return Parent == null; } } public Folder(string name, string fullPath, Folder parent) { Name = name; FullPath = fullPath; Parent = parent; } public void AddFile(FileSnapshot file) { Files.Add(file.FullPath, file); } public void AddDirectory(Folder folder) { Folders.Add(folder.FullPath, folder); } public List GetAllPendingChanges() { var files = new List(); files.AddRange(Files.Values.Where(x => x.Status != FileSnapshot.FileStatus.None)); foreach (var folder in Folders.Values) files.AddRange(folder.GetAllPendingChanges()); return files; } public void DeleteFile(FileSnapshot file) { if (Files.ContainsKey(file.FullPath)) Files.Remove(file.FullPath); } public void DeleteFolder(Folder folder) { if (Folders.ContainsKey(folder.FullPath)) Folders.Remove(folder.FullPath); } public List GetAllFiles() { var files = new List(); files.AddRange(Files.Values); foreach (var dir in Folders.Values) files.AddRange(dir.GetAllFiles()); return files; } } public class FileSnapshot { public Folder TopLevelFolder { get; } public Folder ParentFolder { get; } public string PathNoRoot { get; } private string Root { get; } private int _FileId { get; set; } private string _Name { get; set; } private DateTime _ModifiedDate { get; set; } public DateTime CreatedDate { get; } public long Size { get; } private string _NamingPattern { get; set; } public FileStatus Status { get; set; } public int FileId { get { return _FileId; } set { //Since this file is getting an ID, that means we've been commited to the database, so change the Status to reflect this. Status = FileStatus.None; FileId = value; } } public DateTime ModifiedDate { get { return _ModifiedDate; } set { if (Status != FileStatus.PendingInsert) Status = FileStatus.PendingUpdate; _ModifiedDate = value; } } public string NamingPattern { get { return _NamingPattern; } set { if (Status != FileStatus.PendingInsert) Status = FileStatus.PendingUpdate; _NamingPattern = value; } } public string Name { get { return _Name; } } public FileSnapshot(DB.DBFileSnapshot file, Folder organizationalFolder, Folder parentFolder, string rootPath) { TopLevelFolder = organizationalFolder; ParentFolder = parentFolder; var db = new DB(); if (file.PatternId != 0) _NamingPattern = db.GetNamingPatternById(file.PatternId); PathNoRoot = file.PathNoRoot; Root = rootPath; _Name = file.Name; _ModifiedDate = file.ModifiedDate; CreatedDate = file.CreationDate; Size = file.Size; _FileId = file.Id; Status = FileStatus.None; } public FileSnapshot(FileInfo file, Folder organizationalFolder, Folder parentFolder, string rootPath) { TopLevelFolder = organizationalFolder; ParentFolder = parentFolder; _Name = file.Name; //Add one to the length to remove the trailing slash in the root path. PathNoRoot = file.FullName.Remove(0, rootPath.Length + 1); Root = rootPath; _ModifiedDate = file.LastWriteTimeUtc; CreatedDate = file.CreationTimeUtc; Size = file.Length; _FileId = 0; Status = FileStatus.PendingInsert; } public bool HasOwnNamingPattern { get { return !string.IsNullOrEmpty(NamingPattern); } } public string FullPath { get { return Path.Combine(Root, PathNoRoot); } } public bool HasPendingChanges { get { return Status != FileStatus.None; } } public enum FileStatus { PendingInsert, PendingUpdate, PendingDelete, Deleted, None } } public class SnapshotComparsion { public List NewFiles { get; set; } public List ModifiedFiles { get; set; } public List DeletedFiles { get; set; } public List NewTopLevelFolders { get; set; } public List RenamedFiles { get; set; } public struct RenamedFile { public FileSnapshot From; public FileSnapshot To; } } }