Возвращаемся на раздачу или как сделать невозможное

    Предыстория


    Не так давно, а именно 5 июня хабрачеловек по имени alan008 задал вопрос. Чтобы не заставлять ходить за подробностями, приведу его здесь:

    Нужна помощь!

    За несколько лет с разных трекеров (преимущественно c rutracker'а) разными клиентами (преимущественно uTorrent'ом) скачано много гигабайт разного полезного контента. Скачанные файлы впоследствии вручную перемещались с одного диска на другой, uTorrent их соответственно не видит. Многие .torrent файлы устарели сами по себе (например, велась раздача сериала путем добавления новых серий заменой .torrent файла).

    Теперь сам вопрос: есть ли способ автоматически (не вручную) установить соответствие между имеющимися на компьютере .torrent файлами и содержимым, раскиданным по разным логическим дискам компьютера? Цель: удалить лишние (неактуальные) .torrent файлы, а для актуальных — поставить всё на раздачу. У кого какие идеи? :)

    При необходимости (если это требуется) можно снова поместить все файлы данных в один каталог на одном логическом диске.

    В обсуждениях сошлись на том, что если это и можно сделать, то только ручками. Мне же этот вопрос показался интересным, и после возвращения из отпуска я нашел время, чтобы в нем разобраться.

    Потратив в общей сложности неделю на разбор формата .torrent-файла, поиск нормально работающей библиотеки для его парсинга, я приступил к написанию программы, которая позволит решить проблему затронутую в упомянутом вопросе.

    Прежде чем начать, стоит отметить несколько моментов:
    1. Получилось много, но не все.
    2. По формату файла .torrent будут даны лишь необходимые пояснения.
    3. Людей, чувствительных к временами некачественному коду, прошу меня заранее простить — я знаю, что многое можно было написать лучше, оптимальнее и безглючнее.

    Для тех, кому интересно, что из этого получилось, технические подробности и подводные камни — прошу под кат.


    В данном случае имеется отличный способ решения — перекачать заново. Но мы же не ищем легких путей, да и предлагался такой вариант! Итак, будем решать задачу по сложному — не скачивать.

    Приступая к написанию любой программы, необходимо сначала продумать хотябы базовый алгоритм ее работы. В нашем случае алгоритм, по сути, состоит из двух шагов:

    1. Найти и прочитать все .torrent-файлы;
    2. Найти в куче файлов тот, который соответствует описанному в .torrent, и переместить его в папку, соответствующую пути в .torrent.

    Для реализации своей идеи, я использовал C#, однако для этого подойдет любой язык, на котором есть библиотеки для чтения файлов в формате bencode и реализована возможность считать SHA-1 хеш.

    Ну что же, приступим к решению поставленной задачи.

    Ищем торренты и читаем их



    После того, как я разобрался с устройством .torrent-файлов, передо мной встал вопрос парсинга всего этого чуда. Прошерстив интернет по данному вопросу, я обнаружил несколько .NET-библиотек под это дело. Выбор свой я остановил на, более-менее понятной и работающей из коробки библиотеке BencodeLibrary, однако впоследствии пришлось ее немного дополнить под свои нужды, но об этом позже.

    Начнем с самого простого момента — чтения .torrent.

    Строение .torrent-файла довольно простое — он представляет из себя словарь в формате bencode. В данном словаре нас интересует только пара с ключом info — блок описания файлов. Этот тоже является словарем и содержит в себе информацию об имени файлов, их размере. Кроме того, как многим известно, торрент хеширует файлы не целиком, а по кускам определенной длины, которая зависит от размера этих файлов. Информация о размере этого куска также содержится в словаре info.

    Для хранения информации из прочитанного файла будем использовать такой класс Torrent:
    class Torrent
    public class Torrent
    {
    	public Torrent(string name, List<LostFile> files, int pieceLength, char[] hash, string fileName)
    	{
    		Name = name; 
    		Files = files;
    		PieceLength = pieceLength;
    		Hash = hash;
    		FileName= fileName;
            }
    	
    	public string Name;
    	public int PieceLength;
    	public char[] Hash;
    	public List<LostFile> Files;
    	public string FileName;
            
    	...
    }

    Здесь поля хранят следующую информацию:

    * Name — имя торрента (вообще говоря — имя папки или имя файла)
    * Files — список файлов, которые нам надо будет в дальнейшем искать
    * PieceLength — размер тех самых кусочков, хеш которых нам предстоит считать
    * Hash — хеш строка всех файлов
    * FileName — имя .torrent-файла на диске

    Теперь стоит заострить внимание на строке хеша. Она составляется очень просто. Все файлы склеиваются в один (виртуально конечно) друг за другом, образуя при этом один БОЛЬШОООООООЙ воображаемый файл. В этом воображаемом файле берем кусок длины PieceLength, считаем SHA1 хеш, кладем хеш в строку, берем следующий кусок, считаем хеш, дописываем к концу строки с хешем предыдущего куска. Т. е. это обычная конкатенация хешей всех кусков.



    Как мог заметить внимательный читатель, файл внутри класса — это не просто файл, а особый тип данных, в котором файл описывается некой конструкцией типа LostFile. Вот она:
    class LostFile
    public class LostFile
    {
    	public LostFile(string name, long length, long beginFrom)
    	{
    		Name = name;
    		Length = length;
    		BeginFrom = beginFrom;
    	}
    
    	public string Name;
    	public long Length;
    	public long BeginFrom;
            
    	. . .
    }

    Здесь все просто: имя файла и его размер. Кроме того этот класс содержит еще одно поле — BeginFrom. Оно описывает начало этого файла в том БОЛЛЬШОООООМ воображаемом файле. Он нужен, чтобы взять правильную часть файла для подсчета хеша — ведь длина файла очень редко кратна длине куска.



    Подготовив структуры для хранения необходимой информации, можно приступать к их заполнению.

    С помощью найденной на просторах интернета библиотеки BencodeLibrary мы читаем наш .torrent-файл и выкорчевываем из него блок info:

    List<LostFile> files = new List<LostFile>(); // список файлов, понадобится позднее
    BDict torrent = (BDict)BencodingUtils.DecodeFile(filename, Encoding.UTF8);
    BDict fileInfo = (BDict)torrent["info"];
    

    Далее из этого блока необходимо забрать данные об имени торрента, размере куска.

    string name = ((BString)fileInfo["name"]).Value;
    int pieceLength = (int)((BInt)fileInfo["piece length"]).Value;
    

    С чтением хеша возникла проблема, решение которой мне не очень нравится, но оно работает. Дело в том, что по спецификации все строки в .torrent-файле должны быть в UFT8. Если читать хеш, который по спецификации записан в формате bencode-строки, как UTF8-строку, то возникнет проблема сравнения — хеши одинаковых кусков не совпадут. Читая же торрент в предлагаемой кодировке codepage-437, мы получим проблемы с русскими буквами в путях. Выход из такой ситуации, которая меня тормознула на два дня, я нашел не лучший, но работающий — читать два раза в разных кодировках.

    torrent = (BDict)BencodingUtils.DecodeFile(filename, Encoding.GetEncoding(437));
    char[] pieces = ((BString)((BDict)torrent["info"])["pieces"]).Value.ToCharArray();
    

    В этом месте мы передаем в метод `BencodingUtils.DecodeFile` вторым параметром информацию о кодировке. Это как раз тот момент, когда пришлось добавлять один метод в библиотеку — изначально codepage-437 была вшита в код.

    Мы добрались до самого интересного момента в этой части — чтение информации о файлах. Торрент файлы бывают двух типов. Эти типы различаются тем, сколько файлов в них описано. При описании только одного файла в .torrent пишется его имя и размер.

    Сначала разберем .torrent с описанием одного файла.

    long Length = ((BInt)fileInfo["length"]).Value;
    files.Add(new LostFile(name, Length, 0)); // files - список файлов
    

    Тут все просто — имя торрента совпадает с именем файла. В случае, когда файлов в раздаче много, то в поле name пишется имя папки, в которую их надо положить (на самом деле может быть что угодно, но почему-то все пишут имя папки в которой эти файлы лежали при создании). Кроме того появляется список files в котором содержится информация о каждом файле: путь к нему и размер. Если размер — просто целое число, то путь к файлу представляет собой список из строк (имен директорий), пройдя по которым мы увидим этот файл.

    Такое лучше пояснять на примере. Для файлов level_1\level_2_1\file_1.txt и level_1\level_2_2\file_2.txt, если мы захотим их раздавать, поле name будет содержать имя папки верхнего уровня ("level_1"), а список path для одного из файлов будет следующего вида: {"level_2_1", "file_1.txt"} и {"level_2_2", "file_2.txt"} для другого.

    Нам для .torrent с несколькими файлами надо путь до каждого файла собрать в одну строку. Кроме того, надо хранить начало каждого файла в том БОЛЬШООООМ (не забыли, правда же?!):

    BList filesData = (BList)fileInfo["files"];
    long begin = 0;
    
    foreach (BDict file in filesData)
    {
    	BList filePaths = (BList)file["path"];
    	long length = ((BInt)file["length"]).Value;
    
    	string fullPath = name;
    	foreach (BString partOfPath in filePaths)
    	{
    		fullPath += @"\" + partOfPath.Value;
    	}
    	files.Add(new LostFile(fullPath, length, begin)); // files - список файлов
    
    	begin += length;
    }
    


    Очень важно отметить, что порядок следования файлов в БОЛЬШОООООМ файле может быть любым, не обязательно по алфавиту или по размеру. Но порядок файлов в списке files будет точно таким же. Это ключевой момент для понимания принципа хеширования. Для примера, в ситуации, изображенной на первом рисунке, список файлов будет следующим: {"file_3","file_1", ..., "file_2"}. Таким образом, считая хеш одного файла, мы знаем какой файл надо будет брать следующим.

    Когда мы все это дело прочитали и посчитали — давайте создадим и вернем экземпляр Torrent:

    new Torrent(name, files, pieceLength, pieces, filename);
    

    Собирая теперь все чтение и разбор .torrent-файла воедино, получаем:
    ReadTorrent
    static Torrent ReadTorrent(string filename)
    {
    	List<LostFile> files = new List<LostFile>();
    	BDict torrent = (BDict)BencodingUtils.DecodeFile(filename);
    	BDict fileInfo = (BDict)torrent["info"];
    
    	string name = ((BString)fileInfo["name"]).Value;
    	int pieceLength = (int)((BInt)fileInfo["piece length"]).Value;
    
    	torrent = (BDict)BencodingUtils.DecodeFile(filename, Encoding.GetEncoding(437));
    	char[] pieces = ((BString)((BDict)torrent["info"])["pieces"]).Value.ToCharArray();
    
    	if (fileInfo.ContainsKey("files")) // Multifile torrent
    	{
    		BList filesData = (BList)fileInfo["files"];
    		long begin = 0;
    		
    		foreach (BDict file in filesData)
    		{
    			BList filePaths = (BList)file["path"];
    			long length = ((BInt)file["length"]).Value;
    
    			string fullPath = name;
    			foreach (BString partOfPath in filePaths)
    			{
    				fullPath += @"\" + partOfPath.Value;
    			}
    			files.Add(new LostFile(fullPath, length, begin));
    
    			begin += length;
    		}
    	}
    	else // Singlefile torrent
    	{
    		long Length = ((BInt)fileInfo["length"]).Value;
    		files.Add(new LostFile(name, Length, 1));
    	}
    
    	return new Torrent(name, files, pieceLength, pieces, filename);
    }
    

    Теперь, когда у нас есть все необходимые данные, мы готовы к самому интересному — поиску наших файлов.

    Ищем файлы



    Мы вплотную подошли к реализации второго шага нашего алгоритма. Для этого будем использовать метод FindFiles такого вида:

    void FindFiles(Torrent torrent, List<FileInfo> files, string destinationPath) {}
    

    Здесь files — список файлов, среди которых мы будем искать, destinationPath — путь до папки назначения, в которую будут помещаться найденные файлы.

    Для каждого файла в .torrent мы будем перебирать все файлы из кучи и их сверять. Так как проверка хеша довольно затратна, то надо сначала отсеять явно левые файлы. Ну посудите сами: если я качал дискографию в .mp3 и переместил ее, то явно не менял расширения файлов. Имя мог поменять, а вот расширение вряд ли.

    FileInfo fileOnDisk = files[j];
    if (fileOnDisk.Extension != Path.GetExtension(fileInTorrent.Name))
    	continue;
    

    Также стоит проверять длину файла, но это уже сомнительно и иногда может давать ложные срабатывания. Только после того, как мы отсеяли по расширению явно левые файлы, можно приступать к проверке хеша.

    if (!torrent.CheckHash(i, fileOnDisk))
    	continue;
    

    После того как проверка завершена, и мы удостоверились в соответствии файла искомому — перемещаем его в папку назначения с правильным путем. Перед перемещением будем естественно проверять наличие директории, а также проверим есть ли уже такой файл или нет. copyFile — переменная передаваемая с формы пользователем, ее назначение, я думаю, понятно всем.

    			
    FileInfo fileToMove = new FileInfo(destinationPath + @"\" + fileInTorrent.Name);
    
    if (!Directory.Exists(fileToMove.DirectoryName))
    	Directory.CreateDirectory(fileToMove.DirectoryName);
    
    if (!fileToMove.Exists)
    {
    	// Перемещаем-копируем файл
    	if (copyFile)
    		File.Copy(fileOnDisk.FullName, fileToMove.FullName);
    	else
    		File.Move(fileOnDisk.FullName, fileToMove.FullName);
    		// Убираем из списка рассматириваемых
    	files.Remove(fileOnDisk);
    	// Убираем из описания торрента
    	torrent.Files.RemoveAt(i--);
    	break; //Больше искать файл в дереве не надо, можно переходить к следующему в torrent
    }

    Есть в коде выше три важных для пояснения момента. Начну с двух последних — вот эти строки:

    	
    files.Remove(fileOnDisk);
    torrent.Files.RemoveAt(i--);
    

    Я посчитал вполне логичным убирать уже отсортированные файлы из рассмотрения, что позволит несколько сократить время выполнения поиска. Во второй строке есть конструкция .RemoveAt(i--); так как из коллекции убирается текущий элемент, то указатель надо сдвинуть назад, чтобы на следующей итерации цикла брался следующий элемент, а не через один.

    Теперь про первый момент. Я знаю про наличие foreach для списка, но его при использовании нельзя модифицировать этот спикок, то есть мы не сможем удалять уже ненужные более элементы. Итак, собирая все выше описанное в один метод, имеем:
    ReadTorrent
    				
    public static void FindFiles(Torrent torrent, List<FileInfo> files, string destinationPath, bool copyFile)
    {
    	for (int i = 0; i < torrent.Files.Count; i++)// (LostFile fileInTorrent in torrent.Files)
    	{
    		LostFile fileInTorrent = torrent.Files[i];
    
    		for (int j = 0; j < files.Count; j++)
    		{
    			FileInfo fileOnDisk = files[j];
    
                        // Ищем файл с таким же разширением
    			if (fileOnDisk.Extension != Path.GetExtension(fileInTorrent.Name))
    				continue;
                        // Проверяем размер
    			if (fileOnDisk.Length != fileInTorrent.Length)
    				continue;
                        // Проверяем хэш
    			if (!torrent.CheckHash(i, fileOnDisk))
    				continue;
    
                        // Все проверки пройдены. перед нами искомый файл
                        // Перемещаем его
    			FileInfo fileToMove = new FileInfo(destinationPath + @"\" + fileInTorrent.Name);
    
    			if (!Directory.Exists(fileToMove.DirectoryName))
    				Directory.CreateDirectory(fileToMove.DirectoryName);
    
    			if (!fileToMove.Exists)
    			{
    				if (copyFile)
    					File.Copy(fileOnDisk.FullName, fileToMove.FullName);
    				else
    					File.Move(fileOnDisk.FullName, fileToMove.FullName);
    
                            // И убираем из списка рассматириваемых
    				files.Remove(fileOnDisk);
                            // Убираем из описания торрента
    				torrent.Files.RemoveAt(i--);
    				break;
    			}                    
    		}
    	}
    }
    

    Ну вот! Самое вкусное.

    Проверка хеша



    Как видно из кода выше, для проверки хеша мы передаем имя файла на диске и номер файла в списке файлов торрента. Это надо для того, чтобы не запускать поиск в списке файлов, а сразу взять его по номеру, раз он известен (еще одно "+1" циклу for).

    public class Torrent
    {
    	public string Name;
    	public int PieceLength;
    	public char[] Hash;
    	public List<LostFile> Files;
    	public string FileName;
    		
    	public bool CheckHash(int index, FileInfo fileOnDisk) {}
    }
    

    Теперь приступим к реализации нашего метода проверки хеша. На данном этапе мы знаем номер в списке файлов торрента и путь до файла на диске

    LostFile checkingFile = this.Files[index];
    
    if (checkingFile.Length < this.PieceLength * 2 - 1)
    	return false;
    

    В принципе, мы можем считать хеш любого файла, но давайте немного упростим себе задачу. Мы будем браться только за файлы, длина которых больше или равна PieceLength * 2 - 1. Такое ограничение даст нам возможность вычленить хотя бы один кусок для проверки, полность находящийся в файле. У такого подхода есть несколько существенных плюсов:
    1. Нет необходимости дополнительно искать на диске соседние файлы;
    2. Длина куска для хеширования очень редко превышает 2-4 МБ, что дает нам еще один плюс — с точки зрения производительности и времени, докачать такие файлы намного проще, чем искать их на диске.

    На этом этапе мы подошли к самому сложному из всего, что нам надо было сделать — поиску нужного куска для проверки. Давайте немного отдалимся от программирования и обратимся к математике, сформулировав вспомогательную задачку.

    Когда торрент-клиент проводит хеширование файлов, он считает хеш по порядку, однако бывает так, что нет одного или нескольких файлов. Тогда торрент-клиенту надо знать какой следующий кусок брать и откуда он будет начинаться в следующем имеющемся файле. Для вычисления двух этих цифр будем использовать следующий код, в котором переменная firstChunkNumber содержит номер первого куска, который полностью содержится в данном файле, а bytesOverhead — количество байт от начала файла до начала этого куска. Для лучшего понимания этого момента взгляните на поясняющий рисунок после кода.

    long start = 0;
    long firstChunkNumber = 0;
                
    long bytesOveload = checkingFile.BeginFrom % this.PieceLength;
    
    if (bytesOveload == 0) // Если кусок начинается с первого байта файла
    {
    	start = checkingFile.BeginFrom;
    	firstChunkNumber = checkingFile.BeginFrom / this.PieceLength;
    }
    else
    {
    	firstChunkNumber = checkingFile.BeginFrom / this.PieceLength + 1;
    	start = firstChunkNumber * this.PieceLength - checkingFile.BeginFrom;
    }
    




    Ответить на вопрос «Почему номер куска разный для случая, когда его начало совпадает с началом файла, и для случая, когда кусок лежит внутри?» предлагается самостоятельно.

    Сейчас, зная номер куска мы должны взять его хеш из торрента с помощью такой конструкции:

    char[] hashInTorrent = new char[20]; // 20 - длина хеша SHA1 в байтах
    Array.Copy(this.Hash, firstChunkNumber * 20, hashInTorrent, 0, 20);
    

    После этого, надо прочитать кусок из файла и посчитать его хеш:

    char[] fileHash = new char[this.PieceLength];
    
    using (BinaryReader fs = new BinaryReader(new FileStream(fileOnDisk.FullName, FileMode.Open)))
    {
    	using (SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider())
    	{
    		byte[] piece = new byte[this.PieceLength];
    
    		fs.BaseStream.Position = start;
    		fs.Read(piece, 0, this.PieceLength);
    
    		fileHash = Encoding.GetEncoding(437).GetString(sha1.ComputeHash(piece)).ToCharArray();
    	}
    }
    

    Ну и самое важное — его проверить. У меня, почему-то не захотел работать ни один из методов Equals(), которые я смог найти, поэтому проверяем так:

    for (int i = 0; i < fileHash.Length; i++)
    {
    	if (fileHash[i] != hashInTorrent[i])
    		return false;
    }
    

    Собирая воедино сие творение возбужденного мозга, получим метод следующего содержания:
    CheckHash
    			
    public bool CheckHash(int index, FileInfo fileOnDisk)
    {
    	LostFile checkingFile = this.Files[index];
    
    	if (checkingFile.Length < this.PieceLength * 2 - 1)
    		return false;
    
    	long start = 0;
    	long firstChunkNumber = 0;
                
    	long bytesOveload = checkingFile.BeginFrom % this.PieceLength;
    
    	if (bytesOveload == 0)
    	{
    		start = checkingFile.BeginFrom;
    		firstChunkNumber = checkingFile.BeginFrom / this.PieceLength;
    	}
    	else
    	{
    		firstChunkNumber = checkingFile.BeginFrom / this.PieceLength + 1;
    		start = firstChunkNumber * this.PieceLength - checkingFile.BeginFrom;
    	}
    
    	char[] hashInTorrent = new char[20];
    	Array.Copy(this.Hash, firstChunkNumber * 20, hashInTorrent, 0, 20);
    
    	char[] fileHash = new char[this.PieceLength];
    
    	using (BinaryReader fs = new BinaryReader(new FileStream(fileOnDisk.FullName, FileMode.Open)))
    	{
    		using (SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider())
    		{
    			byte[] piece = new byte[this.PieceLength];
    
    			fs.BaseStream.Position = start;
    			fs.Read(piece, 0, this.PieceLength);
    
    			fileHash = Encoding.GetEncoding(437).GetString(sha1.ComputeHash(piece)).ToCharArray();
    		}
    	}
    			
    	for (int i = 0; i < fileHash.Length; i++)
    		if (fileHash[i] != hashInTorrent[i])
    			return false;
    
    	return true;
    }
    

    На этой прекрасной ноте, рассказ о методах и алгоритмах заканчивается, и мы переходим к рассказу о реализации в реальной жизни данного творения. Вполне понятно, что данная задача мной решалась не для того, чтобы решить, а для того, чтобы реализовать. Поэтому привожу на суд общественности мое творение, которое реализует все то, о чем написано выше.

    Программа



    Программа написана, как уже упоминалось на C#. При работе не сильно прихотлива, требует только .NET 2.0. Есть однако одно ограничение на использование: торрент файлы и коллекцию лучше убрать из корня логического диска. Причина этого ограничения — использование при сканировании директорий параметра `SearchOption.AllDirectories`, что приводит к вылету при попытке прочитать закрытые директории типа корзины или `System Volume Information` (если знающие люди подскажут как это обойти, то буду весьма признателен). Для папки назначения особых ограничений естественно нет, главное чтобы влезло, и можно было в нее писать, иначе вылетит с ошибкой (не моделировал, но логично).

    В процессе работы, после окончания обработки очередного файла, выводится результат — имя .torrent-файла на диске и количество обработанных файлов.



    Чтобы запустить сканирование, необходимо выбрать три директории (с файлами .torrent, с файлами для сортировки и папку для отсортированного), по желанию указать две опции и запустить сканирование.

    По поводу производительности. Она пока что низкая: обработка 10 больших torrent-файлов заняла около 5 минут.

    Так как работает приложение в один поток, то во время выполнения интерфейс подвисает, но я над этим работаю. Также хочу напомнить, что файлы малого размера (меньше 2-х мегабайт) не будут перемещены по причине отсутствия возможности проверки хеша. Вполне вероятны ложные срабатывания из-за того, что проверяется только один кусок под номером firstChunkNumber. Пока что проверять все куски слишком затратно, но в планах есть.

    Не ищите торренты рекурсивно, если они собраны в корне диска.
    Копирование может занять много времени, поэтому интерфейс может подвиснуть — не пугайтесь.


    Так как писалась эта программа 4fun, то качество кода там немного не то, которое хотелось бы, но у меня оно работает. Данная программа не тестировалась, исправлялись только очевидные ошибки, поэтому могут быть, да что скрывать-то, есть скрытые баги. ИСПОЛЬЗУЯ ДАННУЮ ПРОГРАММУ, ВЫ ИСПОЛЬЗУЕТЕ ЕЕ НА СВОЙ СТРАХ И РИСК.

    Взять исходники можно на github. Распространяется по GPLv2. Там есть архив с исполняемым файлом. Для работы требуется библиотека Bencode Library, но не оригинальная, а модифицированная мною (есть у меня в репозитарии, подключена субмодулем).

    Спасибо всем, кто проявил терпение и дочитал эту статью до конца. Рад услышать ваши вопросы, приветствуется всевозможная помощь в совершенствовании алгоритма и, в особенности, кода.

    Источники: BitTorrentSpecification.

    UPD1. По результатам обсуждения мне стало понятно, что правильней будет не ломать существующие коллекции выдергиванием файлов на раздачу, а наоборот — создавать хардлинки в нужном для раздачи месте на файлы внутри упорядоченных коллекций (фильмо и дискографий, например). В дальнейшем программа будет работать именно так.

    UPD2. Если у тех, кто пользовался этой утилитой, есть еще какие-то пожелания по функционалу или баг репорты, то прошу оставлять их на github в issue-трекере.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 77

      +18
      Круто. Молодчина.
        +17
        Мне вообще кажется, что подобный функционал должен быть в самом клиенте.
          +3
          Должна быть! Но мое кунгфу не настолько сильное, чтобы с ним лезть в исходники трансмиссии или рторрента.
            +3
            Зато есть к чему стремиться.
            +2
            Функционал вам ничего не должен. Берите и пишите.
              0
              Как альтернативный вариант — интегрировать в клиент примитивный файл-менеджер, при перемещении файла в котором линки обновляются ИЛИ написать плагин для тотал-коммандера который будет обновлять эти линки
              +1
              Здорово! Чтобы форма не подвисала во время сканирования можно использовать BackgroundWorker
                +1
                Чтобы форма не подвисала во время сканирования можно использовать BackgroundWorker

                Я бы даже сказал нужно. Вообще делать работу в том же потоке, что работает UI — очень дурной тон и годиться только для «quick&dirty» прототипов. На крайняк, если лень писать нормально, но хочется придать поделке товарный вид, есть такая альтернатива как явный вызов обработки сообщений внутри тела рабочего цикла.
                  +2
                  Совершенно верно. На месте автора я бы сделал консольное приложение, а эксперты по UI могут все написать сами, по своим вкусам и желаниям. Ссылка на репозиторий в самом посте.
                    0
                    По-хорошему, нужно делать не GUI-приложение и не консоль, а класс в библиотеке, который потом уже лентяи используют в пару строк в консольной программе, а эстеты напишут UI на каких-нибудь модных WPF и многопоточности.
                      0
                      Тогда бы автору пришлось еще писать консольный фронтенд
                        0
                        Зачем? Точно так же написал бы то, что написал.
                          0
                          Как минимум, весь софт пишется для себя любимого. Надо же как-то это дело запускать самому? А если это оформлять в виде либы, то нужно еще и пускалку делать. Нет, консольное приложение писать всяко проще.
                            0
                            самому — из под отладчика, как же ещё! ;)
                  +1
                  Да, а еще хорошо бы прогресс-бар использовать.
                  0
                  явно не менял расширения файлов. Имя мог поменять, а вот расширение вряд ли

                  Если расширение было в верхнем регистре — поменяю его на нижний практически обязательно.
                    –1
                    Хм… Виноват. Хотя мне кажется, что .net возвращает какой-то один вид. Проверю как будет возможность.
                    0
                    Вообще лично для себя я вижу эту задачу немного иначе.

                    Когда-то я имел отдельные папки по разным трекерам, хранил все torrent-файлы и не переименовывал и не перемещал сами файлы. Сейчас всё уже не так — практически всё скачанное переименовано, пересортировано, и расползлось по разным жёстким дискам. И я, разумеется, не помню сколько-нибудь полного списка того, что и откуда я качал.

                    Соответственно задача в идеальном случае (не знаю на сколько это возможно — может быть это заняло бы дохренища времени):

                    Скачать ВСЕ (ну или хотя бы из определённых разделов, исключив некоторые разделы по нехарактерным для меня темам) torrent-файлы с нескольких трэкеров и просканировать примерно 3 терабайта «файлопомойки» на предмет соответствия искомым хэшам.

                    При этом есть и вовсе нерешаемые проблемы: некоторые файлы шли в архивах и я их разархивировал, некоторые наоборот заархивировал, а в некоторых музыкальных файлах поменял теги так что хоть контент и имеется, это уже другие файлы с другими хэшами.
                      +1
                      частичная проверка хешей поможет от измененных тегов, особенно если размер пайсов небольшой.
                        0
                        Умнó. Не подумал такой возможности. Спасибо.
                          0
                          На самом деле все очень натянуто. К примеру, если поменяется размер тега в начале файла, то будет сдвиг всех данных файла. В этом случае посчитать хеш конечно можно, но с какой позиции его считать? Что уже скорее невозможно.
                            0
                            А вот поменяется ли размер от изменения тегов — опять непонятно. Будет ли приложение добавлять паддинг тегу или сразу запишет данные — зависит только от него. Будет ли сохранен паддинг, или нет — опять не ясно. Все зависит от конкретных реализаций.
                              +1
                              Я пришел к такому выводу — музыку не раздавать вообще. Ну, почти.
                              Т.е. есть две папки:
                              .../music — здесь музыкальная коллекция. Всё красиво, организованно
                              .../INBOX/music — сюда попадает новая музыка. Она лежит какое-то время, раздаётся, а потом, когда руки доходят и файлы меняются, с раздачи снимается.

                              Всё потому что крайне мало треков, где что-нибудь не изменено.
                                0
                                Размер поменяется, а вот раздавать все равно можно будет, но только те части файлов в которых находится непоследственно музыка. Те части, в которых находятся теги, будут, естественно, признаны негодными для раздавания.
                                  0
                                  Как будешь их искать?
                                    0
                                    Сейчас проверяется только первый кусок, и, если его хеш совпадает, то он считается нашим. Можно проверять все куски, которые есть в этом файле (это есть, отлажу и запилю), но тут возникает вопрос, как считать правильность? Можно считать подходящим только тот файл, у которого совпал хеш всех проверенных кусков. А можно выставить «порог доверия», то есть говорить, что файл наш, если совпали, скажем, 75% кусков, независимо от их положения в файле.
                                      0
                                      А если данные сдвинуты на 1 байт? Будешь считать 2м хешей?
                                        0
                                        Если данные сдвинуты на байт, то ни один хеш уже не совпадет. В этом и проблема, из-за которой я пока не хочу использовать этот «порог». Сейчас хочу сделать проверку всего файла, но при первом несовпадении считать «не тем» и поиск продолжать дальше.
                        +9
                        «Найти в куче файлов тот, который соответствует описанному в .torrent, и переместить его в папку, соответствующую пути в .torrent.»
                        На мой взгляд, было бы логичнее оставить файл где есть, а изменить путь к нему в торренте. Например, если у меня расползется по папкам с кривыми путями та же тщательно отструктурированная и проименованная Бондиада (напр. Nekogda.ne.govori.nekogda, как написал недавно безызвестный автор), я буду недоволен.
                          +4
                          Обязательно попробую поковырять в этом направлении. Создайте issue на github, а то могу забыть.
                            +1
                            Меня на гитхабе нет ввиду отсутствия необходимости, извините.
                              +7
                              О как. За отсутствие учетки на гитхабе нынче минусуют? А если у меня реально нет необходимости в его использовании — я недочеловек?
                                +1
                                >За отсутствие учетки на гитхабе нынче минусуют?

                                А скоро вообще морду бить будут! Останавливают тебя на районе, и так «Есть учетка на Гитхабе? А ну ка дай по-быстрому одну issue зарепортить?»
                                  0
                                  «А если найду?» )
                                    0
                                    «Дай аккаунт на хабре, коммент написать, да не боись, я не про линукс»
                                    0
                                    Вами описанное скорее не «за отсутствие», а «за наличие», то есть в духе «до тех пор, пока не даст issue зарепортить, а заодно и учётку угнать».
                                      0
                                      — Ну зачем хамишь? У тебя просят на Github, а ты мне на Bitbucket пихаешь Нехорошо!
                                0
                                такое раздавать в прежней раздаче уже нельзя
                                    +3
                                    Щито? Переместить файл в торрент-клиенте — это одно. Переместить его в торрент-файле — совершенно другое. Информация о имени/пути лежит внутри info-чанка. Идентификационный хеш считается на основе info-чанка. Поменяв 1 букву в имени, мы получим уже другой торрент, о прежней раздачи говорить уже нельзя.
                                      +1
                                      В таком случае затея практически лишена смысла. Подозреваю, что мало кто хранит файлы в оригинальных названиях после завершения закачки, как правило они неудобоваримы или принципом наименования противоречат замыслу пользователя.
                                        0
                                        В таком случае предлагаю расстрелять автора за то, что написал никому не нужное изделие, создал никому не нужный тред, потратил время уважаемые посетителей уважаемого ресурса, да и вообще из-за него идет снижение ВВП.
                                          +2
                                          Воля Ваша, Иосиф Виссарионыч :)
                                          0
                                          «Оригинальные названия» действительно в подавляющем большинстве случаев меня не устраивают. Приходится не лениться и изменять название в процессе добавления торрента в список закачек клиента. В «Мюторренте» (по-моему и в Делюге тоже такое делал) это приводит к тому, что в списке закачек клиента у раздачи остаётся «оригинальное название», а на винте — то, которое было указано при добавлении торрента.
                                          Сори, сонный, пишется темно и вяло. Но думаю, всем понятно, что я подразумеваю.
                                            0
                                            Раздачу в процессе также можно переименовать, если планируется оставить ее надолго, чтобы глаз не резала. По крайней мере в мюторренте.
                                            А так да, изменение названия в процессе выбора места закачки файла — крайне удобная и приятная возможность.
                                          0
                                          Так в клиенте перемещать и надо. У мюторрента в нутрях конфигов примерно тот же формат берётся за основу, что и у торрентов, вроде. Привязка к клиенту, конечно, но, думаю, его популярность очень высока.
                                            0
                                            Если бы все трекеры оперировали расширением BitTorrent, которое добавляет magnet-ссылку, всё было бы гораздо, гораздо прощею
                                              0
                                              Магнеты не имеют отношения к трекерам
                                      0
                                      Да… А ведь хотелось бы просто в клиенте указать — найди файлы этих раздач вон в той папке и раздавай… чтоб он сам все это сделал, никуда не копируя их.
                                        0
                                        Причина этого ограничения — использование при сканировании директорий параметра `SearchOption.AllDirectories`, что приводит к вылету при попытке прочитать закрытые директории типа корзины или `System Volume Information` (если знающие люди подскажут как это обойти, то буду весьма признателен).

                                        Я не сильно знающий человек, но может просто проверять аттрибуты и не искать среди скрытых и системных файлов?
                                          –1
                                          Параметр SearchOption есть либо AllDirectories, либо TopDirectoryOnly. Это можно обойти ручным перебором директорий. Мне этот вариант не нравится, поэтому если не найду ничего лучше, то сделаю его.
                                          +4
                                          А зачем перемещать, может прости симлинк сделать на каждый фаил? Мне кажется это более логичным. На linux/osx лучше даже хардлинки сделать.
                                            0
                                            Хардлинки и на винде есть. Давно. :)
                                            0
                                            Хорошая работа, однако было бы хорошо добавить решим «кандидатов». К примеру, есть у нас торрент с мелкими файлами, части файлов нет, как следствие хеш проверить мы не можем. Однако, используя данные о размере файлов можно попробовать найти все файлы данного размера и поместить их в директорию «кандидаты», а что делать с ними дальше — пусть решает человек.

                                            Но это наверное особого смысла не имеет, торренты с мелкими файлами можно зачастую перекачать, хотя недавно по интернету ходил торрент-файл с 80к мелких картинок.

                                            Еще фича: файлы «частично похожие», когда мы имеем сегменты для проверки хеша, но все хеши не совпадают. Такое может случиться из-за повреждения данных на диске/изначальной недокачанности.
                                              0
                                              Для каждого файла складывать кандидатов в отдельную папку — сильно затратно. К тому же если реальный файл поврежден, то его там может не быть.

                                              Мелкие файлы, даже если их много, перекачать проще, чем искать кандидатов на их место.
                                                0
                                                Ну дело твое, я просто предложил. Можешь их просто проиндексировать и по размеру попробовать подобрать цепь, при каждой попытке считая хеш. Но это просто идея, а не призыв к действию.
                                              +2
                                              Можно попробовать вместо перемещения и переименования файлов создавать для них симлинки с правильными именами в отдельной папке.
                                                +2
                                                Григорий, выше уже поднималась несколько раз просьба. Повторю применительно к своей проблеме (думаю она есть у многих):
                                                Есть библиотека электронных книг, скаченных из разных мест Интернета. С другой стороны эти книги присутствуют во многих раздачах на рутрекере и часто дублируются. Очень хочется встать со своим складом на все раздачи.
                                                Переместить файлы, как Вы реализовали, не получиться по 2м причинам:
                                                1. Они расположены на диске в порядке, удобным для работы
                                                2. На рутрекере файлы многократно повторяются в раздачах, боюсь винчестера не хватит.

                                                спасибо
                                                  0
                                                  Только писать/патчить торрент-клиент, достаточно интересная затея. Правда неприменимо для мелких файлов, так как фактически будут раздаваться только части этих самых файлов.

                                                  К примеру: есть книга размером в 5 мегабайт, эта же книга есть в 10 раздачах. Если раздача начинается с этой книги, то мы уверенно можем раздать ее начало. Конец — только при условии, что ее размер кратен размеру пайса, или у нас есть книга, которая лежит дальше в торренте. К примеру, пусть размер пайса будет равен 2 метрам, тогда мы сможем раздать 4 метра этой книги. Если книга внутри торрента расположена не в начале, то мы не можем раздать и начало, в первых 2 метрах (минус 1 байт) может быть что-то у нас отсутствующее, равно как и в конце. Следовательно, на такой раздаче мы можем раздать только кусочек в 2 метра, хотя у нас все 5 есть.

                                                  Еще недостаток — трекеры, которые не дают скачивать метадату в больших количествах (те самые торренты), вы просто не найдете все раздачи со своей книжкой, особенно если она переименована. Я нечто подобное просил сделать на рутрекере, но меня очень сильно замодерировали.

                                                  А так штука была бы интересной и полезной.
                                                    0
                                                    Я бы зашёл со стороны дедупликации в файловой системе.
                                                      0
                                                      дубли в файловой системе и в торрентах — две большие разницы. Дубли на ФС (и даже на уровне секторов) найти проще.
                                                        0
                                                        Скорее всего имеется в виду такая ситуация: есть два .torrent-файла раздач, в которых присутствует один или несколько одинаковых файлов. Если мы будем копировать найденные на диске файлы, то получится как раз избыточность данных на диске. Это можно решить с помощью хардлинков.
                                                          –1
                                                          Ты можешь найти 2 одинаковых файла в разных торрентах? Как?
                                                            0
                                                            Нет. Пример того, что я имел в виду.

                                                            Нашли два .torrent-файла, в которых есть один одинаковый файл. Мы их скачали, отсортировали для удобства пользования, и у нас на диске остался только один файл (ну в коллекции книг, например, нет смысла держать две одинаковых). И тут мы захотели вернуться на ОБЕ раздачи. Мы, немного изменив код программы, вернули все как было и получили, что файлов одинаковых снова два, и они оба занимают место.

                                                            И вот тут-то хороша система хардлинков.

                                                            А про поиск двух одинаковых файлов в .torrent — не такая большая проблема, если немного изучить строение фалов и переосмыслить статью в контексеt поиска не файлов на диске, а описания файлов в другом файле.
                                                              –1
                                                              «Нашли два .torrent-файла, в которых есть один одинаковый файл» — как нашли? Даже если они уже скачаны.
                                                                0
                                                                Виноват. Ошибся. «Нашли две раздачи».
                                                                  –1
                                                                  Как нашли? Я могу повторять этот вопрос долго.
                                                                    0
                                                                    Когда ползали по трекеру. Нашли две раздачи. Скачали их .torrent-файлы. Закинули их (.torrent-файлы) в клиент, который вытянул раздачи. Каждую в свою папку. Мы, по завершению загрузки, эти две папки взяли и отсортировали как нам надо. НО. У нас в первой раздаче был файл, который есть и в другой (получилось после скачивания два одинаковых файла в разных папках). Мы это спалили и при сортировке один из них удалили.

                                                                    Это предыстория. Теперь мы хотим вернуться на раздачу. Запускаем программу, речь о которой в статье. Она читает эти два .torrent-файла в папке, которую мы ей скормили. И копирует файл дважды — для первого и для второго прочитанных .torrent-файлов.

                                                                    Получилось дублирование файлов, что нам очень не нравится — места много не бывает. Тут как раз на помощь приходит механизм хардлинков.

                                                                    Так понятней?
                                                                      –1
                                                                      У нас в первой раздаче был файл, который есть и в другой (получилось после скачивания два одинаковых файла в разных папках). Мы это спалили и при сортировке один из них удалили.
                                                                      Как нашли, как палили, при пожаре никто не пострадал? Я могу представить, что идет индексация КАЖДОГО файла, сравнение хешей (и даже побайтное сравнение), но это несколько долго. Но это я представляю.

                                                                      Она читает эти два .torrent-файла в папке, которую мы ей скормили. И копирует файл дважды — для первого и для второго прочитанных .torrent-файлов.
                                                                      А вот этого я не понимаю, ибо файлы могут начинаться с разных позиций пайса, все хеши будут другими. Внимание, как найти, что вот этот вот файл есть в обеих раздачах? КАК?

                                                                      Что такое симлинки/хардлинки я прекрасно знаю, про них писать не надо.
                                                                        –1
                                                                        Глазками видимо нашли, своими… Или даже искалкой дублей в файловой системе…
                                                    0
                                                    Все движется в сторону создания хардлинков.
                                                      0
                                                      Ваше имя впишется золотыми буквами в книжные разделы на трекерах. И в каком то смысле добавит порядка (увеличит количество раздающих) в текущий хаос этих разделов.
                                                        0
                                                        С хардлинками есть проблема: я удалил файлы из коллекции (они мне больше не интересны), а место не освобождается. Может, лучше симлинки?
                                                          0
                                                          А висячий симлинк, это нормально? К тому же, в случае жесткой ссылки мы сможем продолжить раздачу.
                                                            0
                                                            симлинк получается даже хуже. при продолжении раздачи торрент-клиент создаст файл в папке коллекции, откуда файл удалили. значит, хардлинк — меньшее неудобство.
                                                              0
                                                              Хардлинк не меньшее неудобство, а нормальное решение. Надоело раздавать — удали раздачу. Надоело слушать/смотреть/читать — удали коллекцию. При таком подходе не надо думать, что удалив одно похеришь другое.
                                                      0
                                                      Для того, чтобы подобных проблем не возникало, существуют опциональные хеши и вроде бы можно иногда встретить пофайловый SHA1, хотя больше всего хотелось бы TTH.

                                                      Как видно в таблице на том же сайте, .torrent метафайлы, богатые TTH'ем, умеют создавать не все создавалки, но EAD TorrentBuild умеет. У TorrentBuild есть проблемы с иностранными символами в именах (он пишет их в ANSI), но есть open source версия, там это может быть исправлено.

                                                      Чтобы прекратить этот кошмар, желательно создавать только метафайлы, богатые TTH, а трекерам желательно ругаться на метафайлы, не богатые TTH, и предлагать пересоздать .torrent другой программой.

                                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                      Самое читаемое