public long AddAfter(PathEntry node, PathEntry entry, long nodePosition = -1) { // If we don't know where the record is, search for it if (nodePosition == -1) GetRecord(node.Path, out node, out nodePosition); entry.PrevRecord = nodePosition; long insertedPosition = WriteRecord(entry); if (node.NextRecord != -1) { // If there is a record after 'node', we need to insert 'entry' before it // So A -> B -> C, insert D after B results in: A -> B -> D -> C (and of course the backwards links.) PathEntry nextRecord = new PathEntry(); GetRecordAt(node.NextRecord, out nextRecord); nextRecord.PrevRecord = insertedPosition; WriteRecordAt(nextRecord, node.NextRecord); entry.NextRecord = node.NextRecord; WriteRecordAt(entry, insertedPosition); } node.NextRecord = insertedPosition; WriteRecordAt(node, nodePosition); return insertedPosition; }
static bool SelectFilesToKeep(HashPointers ptr, out List<int> toKeep) { bool selectionSuccess = false; toKeep = new List<int>(Enumerable.Range(1, ptr.FileEntries.Count)); bool decided = false; int choice = 0; bool canAutoSelect = false; List<int> oldestIDs = new List<int>(); List<int> newestIDs = new List<int>(); { // Read and register the timestamp when the files were last accessed // The oldest file (lowest timestamp) will be on the top of the list SortedList<DateTime, List<int>> timeStamps = new SortedList<DateTime, List<int>>(ptr.FileEntries.Count); PathEntry entry = new PathEntry(); int currentID = 1; foreach (long offset in ptr.FileEntries) { PathsFile.GetRecordAt(offset, out entry); FileInfo fi = new FileInfo(entry.Path); IEnumerable<DateTime> tsRegistered = timeStamps.Select(ts => ts.Key) .Where(ts => ts.Date == fi.LastAccessTime.Date && ts.Hour == fi.LastAccessTime.Hour && ts.Minute == fi.LastAccessTime.Minute && ts.Second == fi.LastAccessTime.Second); if (tsRegistered.Count() == 1) timeStamps[tsRegistered.First()].Add(currentID); else { List<int> idList = new List<int>(1); idList.Add(currentID); timeStamps.Add(fi.LastAccessTime, idList); } ++currentID; } // If the oldest and newest files are the same, don't select any of them if (timeStamps.Count == 1 && (AutoOldest || AutoNewest)) Console.WriteLine("The files' age are equal. Unable to select oldest and newest ones."); else { oldestIDs.AddRange(timeStamps.First().Value); newestIDs.AddRange(timeStamps.Last().Value); canAutoSelect = true; } } while (!selectionSuccess) { Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine(new String('-', Console.WindowWidth - 1)); DuplicateFileLog.WriteLine(new String('-', 24)); Console.WriteLine("The following " + ptr.FileEntries.Count + " files are duplicate of each other"); DuplicateFileLog.WriteLine("The following " + ptr.FileEntries.Count + " files are duplicate of each other"); Console.ResetColor(); if (Verbose) { Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine("Hash: " + ptr.Hash); DuplicateFileLog.WriteLine("Hash: " + ptr.Hash); Console.ResetColor(); } if (!DryRun) { Console.Write("Files marked "); Console.ForegroundColor = ConsoleColor.White; Console.Write("[ KEEP ]"); Console.ResetColor(); Console.Write(" will be kept. Files marked "); Console.ForegroundColor = ConsoleColor.Red; Console.Write("[DELETE]"); Console.ResetColor(); Console.WriteLine(" will be deleted."); if (!AutoNewest && !AutoOldest) Console.WriteLine("Please select the files you wish to keep or delete."); else if (!canAutoSelect) { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("Was unable to automatically select " + (AutoOldest ? "oldest" : "newest") + " file to keep"); Console.ResetColor(); } else { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("Automatically selecting the " + (AutoOldest ? "OLDEST" : "NEWEST") + " file to keep"); Console.ResetColor(); toKeep.Clear(); if (AutoOldest) toKeep.AddRange(oldestIDs); else if (AutoNewest) toKeep.AddRange(newestIDs); } } // Print the file list with a choice int menuId = 1; PathEntry etr = new PathEntry(); int totalLog10ofEntries = (int)Math.Floor(Math.Log10((double)ptr.FileEntries.Count)) + 1; if (totalLog10ofEntries < 3) // Make sure "-1." can be printed totalLog10ofEntries = 3; foreach (long offset in ptr.FileEntries) { PathsFile.GetRecordAt(offset, out etr); // Create a little menu for the user to give a choice // The length of the choice option printed, how many characters it takes in base 10 int strCurrentLength = (int)Math.Floor(Math.Log10((double)menuId)) + 1; // 0-9: 1 long, 10-99: 2 long, etc. ++strCurrentLength; // The '.' (dot) takes up another character if (!DryRun) { if (toKeep.Contains(menuId)) { Console.ForegroundColor = ConsoleColor.White; Console.Write("[ KEEP ] "); Console.ResetColor(); } else { Console.ForegroundColor = ConsoleColor.Red; Console.Write("[DELETE] "); Console.ResetColor(); } } Console.ForegroundColor = ConsoleColor.Cyan; Console.Write(new String(' ', totalLog10ofEntries - strCurrentLength + 1) + menuId + ". "); bool oldestOrNewest = false; if (oldestIDs.Contains(menuId)) { Console.ForegroundColor = ConsoleColor.White; Console.Write("[OLDEST] "); DuplicateFileLog.Write("[OLDEST] "); oldestOrNewest = true; } else if (newestIDs.Contains(menuId)) { Console.ForegroundColor = ConsoleColor.White; Console.Write("[NEWEST] "); DuplicateFileLog.Write("[NEWEST] "); oldestOrNewest = true; } DuplicateFileLog.Write(new String(' ', (!oldestOrNewest ? 9 : 0) + totalLog10ofEntries - strCurrentLength + 1) + menuId + ". "); Console.ResetColor(); Console.WriteLine(etr.Path); DuplicateFileLog.WriteLine(etr.Path); ++menuId; } if (!AutoNewest && !AutoOldest && !DryRun) { Console.ForegroundColor = ConsoleColor.Magenta; Console.Write(" [DONE] " + new String(' ', totalLog10ofEntries - 2 + 1) + "0. "); Console.ResetColor(); Console.WriteLine("Finalise the choices"); Console.ForegroundColor = ConsoleColor.Green; Console.Write(" [SKIP] " + new String(' ', totalLog10ofEntries - 3 + 1) + "-1. "); Console.ResetColor(); Console.WriteLine("Keep everything for now, decide later"); Console.ForegroundColor = ConsoleColor.DarkRed; Console.Write(" [NUKE] " + new String(' ', totalLog10ofEntries - 3 + 1) + "-2. "); Console.Write("Delete ALL FILES!"); Console.ResetColor(); Console.WriteLine(); } // Read the user's choice if (!AutoNewest && !AutoOldest && !DryRun) { Console.WriteLine("Please select an option from above. If you select a file, its status will be togged between keep and delete."); Console.Write("? "); try { choice = Convert.ToInt32(Console.ReadLine()); selectionSuccess = true; // Attempt to say that the user successfully selected if (choice >= menuId || choice < -2) throw new ArgumentOutOfRangeException("The entered choice is invalid."); } catch (Exception) { Console.ForegroundColor = ConsoleColor.Red; Console.Write("Invalid input. "); Console.ResetColor(); Console.WriteLine("The enterd input is not a number or is out of range. Please select from the presented choices!"); selectionSuccess = false; } } else // If the user decided to automatically keep oldest or newest file, the selection was successful. // Either the oldest or the newest file were selected, or if not, all files were selected to be kept. selectionSuccess = true; if (selectionSuccess) { // Change the buffer list of which files to keep or don't if (choice >= 1) { if (toKeep.Contains(choice)) toKeep.Remove(choice); else toKeep.Add(choice); selectionSuccess = false; // Let the user make further changes decided = false; } else if (choice == -2) { toKeep.Clear(); decided = true; } else decided = (choice == 0); // If -1, tell that the user hasn't decided } } toKeep.Sort(); return decided; }
static void ReadFileSizes(string directory, ref List<string> subfolderList) { if (Verbose) { Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine("Reading contents of " + directory); Console.ResetColor(); } try { int insertIndex = 0; foreach (string path in Directory.EnumerateFileSystemEntries(directory, "*", SearchOption.TopDirectoryOnly)) { string relativePath = Path.GetFullPath(path).Replace(Directory.GetCurrentDirectory(), String.Empty).TrimStart('\\'); // Skip some files which should not be access by the program if (Path.GetFullPath(path) == SizesFile.Stream.Name || Path.GetFullPath(path) == PathsFile.Stream.Name || Path.GetFullPath(path) == HashesFile.Stream.Name || Path.GetFullPath(path) == FilesToRemove.Name || Path.GetFullPath(path) == ((FileStream)DuplicateFileLog.BaseStream).Name) continue; // Skip files if they are in a Subversion structure // SVN saves a "pristine" copy of every file, and this makes every SVNd file to be marked as duplicate. if (relativePath.Contains(".svn\\pristine") || relativePath.Contains(".svn\\entries") || relativePath.Contains(".svn\\format")) continue; try { if (Directory.Exists(relativePath)) { // If it is a directory, add it to the list of subfolders to check later on if (Verbose) Console.WriteLine(relativePath + " is a subfolder."); // Add the found subfolders to the beginning of the list, but keep their natural order subfolderList.Insert(++insertIndex, relativePath); } else if (File.Exists(relativePath)) { if (Verbose) Console.Write("Measuring " + relativePath + "..."); // If it is a file, register its size and the count for its size FileInfo fi = new FileInfo(relativePath); try { SizeEntry entry = new SizeEntry(); long position = 0; bool known = SizesFile.GetRecord((ulong)fi.Length, out entry, out position); entry.Size = (ulong)fi.Length; if (!known) { // Need to reset the entry's count because GetRecord gives // undefined value if the entry is not found. entry.Count = 0; ++SizeCount; // The new size record currently has no associated PathEntry records in the path file. entry.FirstPath = -1; entry.LastPath = -1; entry.HashEntry = -1; } entry.Count++; // Also register its path PathEntry pathRec = new PathEntry(relativePath); long pathWrittenPosition; if (entry.LastPath != -1) { PathEntry previousLastEntry = new PathEntry(); PathsFile.GetRecordAt(entry.LastPath, out previousLastEntry); pathWrittenPosition = PathsFile.AddAfter(previousLastEntry, pathRec, entry.LastPath); } else { pathWrittenPosition = PathsFile.WriteRecord(pathRec); entry.FirstPath = pathWrittenPosition; } entry.LastPath = pathWrittenPosition; SizesFile.WriteRecord(entry); if (Verbose) Console.WriteLine(" Size: " + fi.Length + " bytes."); ++FileCount; VisualGlyph(FileCount); } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("There was an error registering " + relativePath + " in the databank."); Console.ResetColor(); Console.WriteLine(ex.Message); Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("This indicates an error with the databank. Execution cannot continue."); Console.ResetColor(); Console.ReadLine(); Environment.Exit(1); } } } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("The path " + relativePath + " could not be accessed, because:"); Console.ResetColor(); Console.WriteLine(ex.Message); } } } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("The directory " + directory + " could not be accessed, because:"); Console.ResetColor(); Console.WriteLine(ex.Message); } subfolderList.Remove(directory); }
static void Main(string[] args) { Console.WriteLine("Duplicate Destroyer"); Console.WriteLine("'Devastating Desert'"); Console.WriteLine("Licenced under Tiny Driplet Licence (can be found at cloudchiller.net)"); Console.WriteLine("Copyright, Copydrunk, Copypone (c) 2012-2014, Cloud Chiller"); Console.WriteLine(); if (args.Contains("-h")) { Console.WriteLine("HELP:"); Console.WriteLine("-h Show this help text"); Console.WriteLine("-v Verbose mode"); Console.WriteLine("-d Dry run/discovery - Only check for duplicates, but don't actually remove them"); Console.WriteLine("-o Automatically keep the OLDEST of the files"); Console.WriteLine("-n Automatically keep the NEWEST of the files"); Console.WriteLine(); Console.WriteLine("Omitting both -o and -n results in the user being queried about which file to keep."); Console.WriteLine("Using both -o and -n throws an error."); Console.WriteLine(); Environment.Exit(0); } Verbose = args.Contains("-v"); DryRun = args.Contains("-d"); AutoOldest = args.Contains("-o"); AutoNewest = args.Contains("-n"); SizeCount = 0; FileCount = 0; if (AutoOldest == true && AutoNewest == true) { Console.WriteLine("ERROR: Conflicting arguments."); Console.WriteLine("Please use either -o or -n, not both."); Console.WriteLine(); Environment.Exit(3); } FileStream SizesFileStream = null; FileStream PathsFileStream = null; FileStream HashesFileStream = null; FileStream DuplicateLogFileStream = null; try { SizesFileStream = new FileStream(".dd_sizes", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); SizesFileStream.SetLength(0); PathsFileStream = new FileStream(".dd_files", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); PathsFileStream.SetLength(0); HashesFileStream = new FileStream(".dd_hashes", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); HashesFileStream.SetLength(0); FilesToRemove = new FileStream(".dd_remove", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); FilesToRemove.SetLength(0); DuplicateLogFileStream = new FileStream("duplicates_" + DateTime.Now.ToString().Replace(":", "_") + ".log", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); DuplicateLogFileStream.SetLength(0); } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("Was unable to create the program's datafiles."); Console.ResetColor(); Console.WriteLine("Please make sure the folder " + Directory.GetCurrentDirectory() + " is writable."); Console.WriteLine("The following error happened: " + ex.Message); Environment.Exit(1); } SizesFile = new SizeFile(SizesFileStream); PathsFile = new PathFile(PathsFileStream); HashesFile = new HashFile(HashesFileStream); DuplicateFileLog = new StreamWriter(DuplicateLogFileStream); FileRemoveException = false; TargetDirectory = Directory.GetCurrentDirectory(); { Console.Write("Counting files and measuring sizes... " + (Verbose ? "\n" : String.Empty)); List<string> Subfolders = new List<string>(); Subfolders.Add(TargetDirectory); while (Subfolders.Count != 0) { // Read the files in the subfolders. ReadFileSizes(Subfolders[0], ref Subfolders); // The on-the-fly detected subfolders are added to the list while reading. } SizesFile.Stream.Flush(true); PathsFile.Stream.Flush(true); Console.WriteLine((!Verbose ? "\n" : String.Empty) + FileCount + " files found."); Console.WriteLine(); } { Console.Write("Analysing sizes... " + (Verbose ? "\n" : String.Empty)); AnalyseSizes(); SizesFile.DeleteRecord(0); // 0-byte files are ALWAYS duplicates of each other... SizesFile.Stream.Flush(true); PathsFile.Stream.Flush(true); Console.WriteLine((!Verbose ? "\n" : String.Empty) + SizeCount + " unique file size found for " + FileCount + " files."); Console.WriteLine(); } //{ // // Remove entries from the PathsFile physically which were logically removed (marked deleted) in the previous step // if (Verbose) // { // Console.WriteLine("Removing knowledge about files I don't need to check."); // Console.WriteLine("(This is an internal maintenance run to speed up further operations.)"); // } // PathsFile.Consolidate(new SizeFileAligner(Program.AlignSizeFilePointers)); // PathsFile.Stream.Flush(true); // if (Verbose) // Console.WriteLine(); //} { Console.Write("Reading file contents... " + (Verbose ? "\n" : String.Empty)); MD5CryptoServiceProvider mcsp = new MD5CryptoServiceProvider(); ulong _hashesReadCount = 0; foreach (SizeEntry duplicated_size in SizesFile.GetRecords()) { if (Verbose) { Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine("Reading files of " + duplicated_size.Size + " size"); Console.ResetColor(); } // For each size entry, iterate the path list PathEntry entry; long position = duplicated_size.FirstPath; while (position != -1) { if (PathsFile.GetRecordAt(position, out entry)) { string hash = String.Empty; try { hash = CalculateHash(ref mcsp, entry.Path); ++_hashesReadCount; } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("The file " + entry.Path + " could not be checked, because:"); Console.ResetColor(); Console.WriteLine(ex.Message); } if (!String.IsNullOrEmpty(hash)) entry.Hash = hash; else // Mark this record "deleted" so it won't be checked for hash duplication entry.Deleted = true; PathsFile.WriteRecordAt(entry, position); VisualGlyph(_hashesReadCount); position = entry.NextRecord; // Jump to the next record in the chain } } } PathsFile.Stream.Flush(true); Console.WriteLine((!Verbose ? "\n" : String.Empty) + _hashesReadCount + " files read."); } { Console.Write("Searching for true duplication... " + (Verbose ? "\n" : String.Empty)); long UniqueHashCount, DuplicatedFileCount; AnalyseFilelist(out UniqueHashCount, out DuplicatedFileCount); HashesFile.Stream.Flush(true); Console.WriteLine((!Verbose ? "\n" : String.Empty) + UniqueHashCount + " unique content duplicated across " + DuplicatedFileCount + " files."); Console.WriteLine(); Console.WriteLine(); Console.WriteLine("Please select which files you wish to remove."); long dealtWithCount = 0; while (dealtWithCount < UniqueHashCount) { // We go through every hash entry and prompt the user to decide which file to remove HashesFile.Stream.Seek(0, SeekOrigin.Begin); SizeHashEntry she = new SizeHashEntry(); PathEntry etr = new PathEntry(); long pos = 0; while (pos != -1) { // Get the next duplicated hash pos = HashesFile.GetNextRecord(out she); if (pos != -1) { // Iterate the hash pointers... foreach (HashPointers ptr in she.Pointers) { if (ptr.FileEntries.Count == 0) continue; // Select which file the user wants to keep List<int> fileIDsToKeep; bool userDecided = SelectFilesToKeep(ptr, out fileIDsToKeep); if (!DryRun) { if (!userDecided) Console.WriteLine("Didn't make a decision. You will be asked later on."); else { ++dealtWithCount; if (fileIDsToKeep.Count == ptr.FileEntries.Count) Console.WriteLine("Selected to keep all files."); else if (fileIDsToKeep.Count > 0) { if (!AutoOldest && !AutoNewest) { foreach (int id in fileIDsToKeep) { Console.Write("Selected to "); Console.ForegroundColor = ConsoleColor.White; Console.Write("KEEP"); Console.ResetColor(); Console.Write(" "); PathsFile.GetRecordAt(ptr.FileEntries[id - 1], out etr); Console.WriteLine(etr.Path); } foreach (int id in Enumerable.Range(1, ptr.FileEntries.Count).Except(fileIDsToKeep)) { Console.Write("Selected to "); Console.ForegroundColor = ConsoleColor.Red; Console.Write("DELETE"); Console.ResetColor(); Console.Write(" "); PathsFile.GetRecordAt(ptr.FileEntries[id - 1], out etr); Console.WriteLine(etr.Path); byte[] pathLine = Encoding.UTF8.GetBytes(etr.Path + StreamWriter.Null.NewLine); FilesToRemove.Write(pathLine, 0, pathLine.Length); } } } else if (fileIDsToKeep.Count == 0) { Console.WriteLine("All files will be deleted:"); foreach (long offset in ptr.FileEntries) { PathsFile.GetRecordAt(offset, out etr); Console.WriteLine(etr.Path); byte[] pathLine = Encoding.UTF8.GetBytes(etr.Path + StreamWriter.Null.NewLine); FilesToRemove.Write(pathLine, 0, pathLine.Length); } } FilesToRemove.Flush(); } } else ++dealtWithCount; } } } } Console.WriteLine(); } { Console.Write("Removing all scheduled files... " + (Verbose ? "\n" : String.Empty)); uint _filesRemoved = 0; if (DryRun) Console.WriteLine("Won't remove files in dry-run/discovery mode."); else { FilesToRemove.Seek(0, SeekOrigin.Begin); string path; if (FilesToRemove.Length > 0) // Only if there are files to be removed { using (StreamReader sr = new StreamReader(FilesToRemove)) { path = sr.ReadLine(); if (RemoveFile(path)) ++_filesRemoved; } } } Console.WriteLine((!Verbose ? "\n" : String.Empty) + _filesRemoved + " files deleted successfully."); } SizesFileStream.Dispose(); PathsFileStream.Dispose(); HashesFileStream.Dispose(); //FilesToRemove.Dispose(); DuplicateFileLog.Dispose(); // Cleanup //File.Delete(".dd_sizes"); //File.Delete(".dd_files"); //File.Delete(".dd_hashes"); //File.Delete(".dd_remove"); if (FileRemoveException) { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("One or more files could not be deleted."); Console.ResetColor(); } Console.WriteLine("Press ENTER to exit..."); Console.ReadLine(); if (FileRemoveException) Environment.Exit(2); else Environment.Exit(0); }
internal IEnumerable<PathEntry> GetRecords(bool skipDeleted = false) { this.Stream.Seek(0, SeekOrigin.Begin); long position = 0; using (BinaryReader br = new BinaryReader(this.Stream, Encoding.UTF8, true)) while (position < this.Stream.Length) { this.Stream.Seek(position, SeekOrigin.Begin); PathEntry record = new PathEntry(); record.Deleted = br.ReadBoolean(); // 1 record.PrevRecord = br.ReadInt64(); // 8 record.NextRecord = br.ReadInt64(); // 8 record.PathLength = br.ReadUInt16(); // Path length (2) byte[] path_bytes = br.ReadBytes(record.PathLength); // n record.Path = Encoding.UTF8.GetString(path_bytes); record.Hash = Encoding.UTF8.GetString(br.ReadBytes(32)); // 32 position = this.Stream.Position; if (!skipDeleted || !record.Deleted) yield return record; } yield break; }
public long WriteRecord(PathEntry rec) { long pos = -1; this.Stream.Seek(0, SeekOrigin.End); using (BinaryWriter bw = new BinaryWriter(this.Stream, Encoding.UTF8, true)) { pos = bw.BaseStream.Position; bw.Write(rec.Deleted); // 1 bw.Write(rec.PrevRecord); // 8 bw.Write(rec.NextRecord); // 8 bw.Write(rec.PathLength); // 2 bw.Write(Encoding.UTF8.GetBytes(rec.Path)); // n bw.Write(Encoding.UTF8.GetBytes(rec.Hash)); // 32 } this.Stream.Flush(); return pos; }
public void WriteRecordAt(PathEntry rec, long pos) { this.Stream.Seek(pos, SeekOrigin.Begin); using (BinaryWriter bw = new BinaryWriter(this.Stream, Encoding.UTF8, true)) { bw.Write(rec.Deleted); // 1 bw.Write(rec.PrevRecord); // 8 bw.Write(rec.NextRecord); // 8 bw.Write(rec.PathLength); // 2 bw.Write(Encoding.UTF8.GetBytes(rec.Path)); // n bw.Write(Encoding.UTF8.GetBytes(rec.Hash)); // 32 } this.Stream.Flush(); }
public IEnumerable<PathEntry> GetRecords(string path, long pos = -1, bool traverseBackwards = false) { PathEntry entry = new PathEntry(); // Try to find the record given for traversal bool recordFound; if (pos == -1) recordFound = GetRecord(path, out entry, out pos); else { recordFound = GetRecordAt(pos, out entry); // GetRecordAt doesn't check just gives the record as result. // Bail out if the given record is not the right one we searched for... if (entry.Path != path) recordFound = false; } // Get the rest of the records and given them in the enumerable if (recordFound) { yield return entry; long nextPositionToRead; if (traverseBackwards) nextPositionToRead = entry.PrevRecord; else nextPositionToRead = entry.NextRecord; while (nextPositionToRead != -1) { if (GetRecordAt(nextPositionToRead, out entry)) { yield return entry; if (traverseBackwards) nextPositionToRead = entry.PrevRecord; else nextPositionToRead = entry.NextRecord; } else break; } } yield break; }
public bool GetRecordAt(long position, out PathEntry record) { record = new PathEntry(); if (position > this.Stream.Length) throw new ArgumentOutOfRangeException("Position out of stream bounds."); using (BinaryReader br = new BinaryReader(this.Stream, Encoding.UTF8, true)) { try { this.Stream.Seek(position, SeekOrigin.Begin); record.Deleted = br.ReadBoolean(); // 1 record.PrevRecord = br.ReadInt64(); // 8 record.NextRecord = br.ReadInt64(); // 8 record.PathLength = br.ReadUInt16(); // Path length (2) byte[] path_bytes = br.ReadBytes(record.PathLength); // n record.Path = Encoding.UTF8.GetString(path_bytes); record.Hash = Encoding.UTF8.GetString(br.ReadBytes(32)); // 32 } catch (Exception e) { record.Deleted = true; //record.PrevRecord = -1; //record.NextRecord = -1; //record.PathLength = 0; record.Path = e.Message; //record.Hash = String.Empty; return false; } } return true; }
public bool GetRecord(string path, out PathEntry record, out long position) { record = new PathEntry(); position = 0; if (this.Stream.Length == 0) return false; // Search for the record linearly this.Stream.Seek(0, SeekOrigin.Begin); bool found = false; using (BinaryReader br = new BinaryReader(this.Stream, Encoding.UTF8, true)) { while (this.Stream.Position < this.Stream.Length && !found) { // Read every record and try to make out if it is the searched one. position = this.Stream.Position; record.Deleted = br.ReadBoolean(); // 1 record.PrevRecord = br.ReadInt64(); // 8 record.NextRecord = br.ReadInt64(); // 8 record.PathLength = br.ReadUInt16(); // Path length. byte[] path_bytes = br.ReadBytes(record.PathLength); // n record.Path = Encoding.UTF8.GetString(path_bytes); record.Hash = Encoding.UTF8.GetString(br.ReadBytes(32)); // 32 if (record.Path == path && !record.Deleted) found = true; } } return found; }
public void DeleteRecord(PathEntry rec, long position) { // Check if the given record is the one we want to delete. PathEntry already = new PathEntry(); if (!GetRecordAt(position, out already)) throw new ArgumentOutOfRangeException("There is no record at the given position."); if (rec.Path != already.Path) throw new ArgumentException("The record at the given position does not match the given record."); // When a record is deleted, the broken chain that was going through them has to be reconnected // A -> B -> C with B's deletion becomes A -> C if (already.PrevRecord != -1) { PathEntry prevRec = new PathEntry(); GetRecordAt(already.PrevRecord, out prevRec); // If there is something after the deleted one, it comes after the previous one if (already.NextRecord != -1) prevRec.NextRecord = already.NextRecord; else prevRec.NextRecord = -1; WriteRecordAt(prevRec, already.PrevRecord); } if (already.NextRecord != -1) { PathEntry nextRec = new PathEntry(); GetRecordAt(already.NextRecord, out nextRec); // If there is something before the deleted one, it comes before the next one if (already.PrevRecord != -1) nextRec.PrevRecord = already.PrevRecord; else nextRec.PrevRecord = -1; WriteRecordAt(nextRec, already.NextRecord); } // Mark the current record deleted and break its chains already.PrevRecord = -1; already.NextRecord = -1; already.Deleted = true; WriteRecordAt(already, position); }
public void DeleteRecord(string path) { PathEntry rec = new PathEntry(); long pos = 0; if (GetRecord(path, out rec, out pos)) DeleteRecord(rec, pos); }