seria/src/Torrents/TorrentBuilder.php

206 lines
6.6 KiB
PHP

<?php
namespace Seria\Torrents;
use Exception;
use InvalidArgumentException;
use RuntimeException;
use Index\Data\DbTools;
use Index\Data\IDbTransactions;
use Index\Serialisation\Bencode;
use Seria\Users\UserInfo;
class TorrentBuilder {
private ?string $userId = null;
private string $name = '';
private int $created;
private int $pieceLength = 0;
private bool $isPrivate = false;
private string $comment = '';
private array $files = [];
private array $pieces = [];
public function __construct() {
$this->created = time();
}
public function setUser(?UserInfo $userInfo): self {
return $this->setUserId($userInfo?->getId());
}
public function setUserId(?string $userId): self {
$this->userId = $userId;
return $this;
}
public function setName(string $name): self {
if(empty($name))
throw new InvalidArgumentException('$name may not be empty.');
$this->name = $name;
return $this;
}
public function setCreatedTime(int $created): self {
if($created < 0 || $created > 0x7FFFFFFF)
throw new InvalidArgumentException('$created is not a valid timestamp.');
$this->created = $created;
return $this;
}
public function setPieceLength(int $pieceLength): self {
if($pieceLength < 1)
throw new InvalidArgumentException('$pieceLength is not a valid piece length.');
$this->pieceLength = $pieceLength;
return $this;
}
public function setPrivate(bool $private): self {
$this->isPrivate = $private;
return $this;
}
public function setComment(string $comment): self {
$this->comment = $comment;
return $this;
}
public function addFile(string|array $path, int $length): self {
if(is_array($path))
$path = implode('/', $path);
$path = trim($path, '/');
if(array_key_exists($path, $this->files))
throw new RuntimeException('Duplicate file.');
$this->files[$path] = [
'length' => $length,
'path' => explode('/', $path),
];
return $this;
}
public function addPiece(string $hash): self {
if(strlen($hash) !== 20)
throw new InvalidArgumentException('$hash is not a valid piece hash.');
$this->pieces[] = $hash;
return $this;
}
public function calculateInfoHash(): string {
$info = [
'files' => array_values($this->files),
'name' => $this->name,
'piece length' => $this->pieceLength,
'pieces' => implode($this->pieces),
];
if(!empty($this->isPrivate))
$info['private'] = 1;
return hash('sha1', Bencode::encode($info), true);
}
public function create(IDbTransactions $dbConn, TorrentsContext $torrentsCtx): string {
$torrents = $torrentsCtx->getTorrents();
$pieces = $torrentsCtx->getPieces();
$files = $torrentsCtx->getFiles();
$dbConn->beginTransaction();
try {
$infoHash = $this->calculateInfoHash();
$torrentId = $torrents->createTorrent(
$this->userId, $infoHash, $this->name, $this->created,
$this->pieceLength, $this->isPrivate, $this->comment
);
foreach($this->files as $file)
$files->createFile($torrentId, $file['length'], $file['path']);
foreach($this->pieces as $piece)
$pieces->createPiece($torrentId, $piece);
$dbConn->commit();
} catch(Exception $ex) {
$dbConn->rollBack();
throw $ex;
}
return $torrentId;
}
public static function import(
TorrentInfo $torrent,
array $pieces,
array $files
): self {
$builder = new TorrentBuilder;
$builder->setUserId($torrent->getUserId());
$builder->setName($torrent->getName());
$builder->setPieceLength($torrent->getPieceLength());
$builder->setPrivate($torrent->isPrivate());
$builder->setCreatedTime($torrent->getCreatedTime());
$builder->setComment($torrent->getComment());
foreach($pieces as $piece)
$builder->addPiece($piece->getHash());
foreach($files as $file)
$builder->addFile($file->getPath(), $file->getLength());
return $builder;
}
public static function decode(mixed $source): self {
if(is_string($source) || is_resource($source))
$source = Bencode::decode($source);
if(!isset($source['info']) || !is_array($source['info']))
throw new InvalidArgumentException('info key missing.');
if(!isset($source['info']['name']) || !is_string($source['info']['name']))
throw new InvalidArgumentException('info.name key missing.');
if(!isset($source['info']['files']) || !is_array($source['info']['files']))
throw new InvalidArgumentException('info.files key missing.');
if(!isset($source['info']['pieces']) || !is_string($source['info']['pieces']))
throw new InvalidArgumentException('info.pieces key missing.');
if(!isset($source['info']['piece length']) || !is_int($source['info']['piece length']))
throw new InvalidArgumentException('info.piece length key missing.');
$builder = new TorrentBuilder;
$builder->setName($source['info']['name']);
$builder->setPieceLength($source['info']['piece length']);
$builder->setPrivate(!empty($source['info']['private']));
if(isset($source['creation date'])
&& is_int($source['creation date']))
$builder->setCreatedTime($source['creation date']);
if(!empty($source['comment']))
$builder->setComment($source['comment']);
foreach($source['info']['files'] as $file) {
if(empty($file)
|| !is_array($file)
|| !isset($file['length'])
|| !is_int($file['length'])
|| !isset($file['path'])
|| !is_array($file['path']))
throw new InvalidArgumentException('Invalid info.files entry.');
foreach($file['path'] as $pathPart)
if(!is_string($pathPart))
throw new InvalidArgumentException('Invalid info.files entry path.');
$builder->addFile($file['path'], $file['length']);
}
$pieces = str_split($source['info']['pieces'], 20);
foreach($pieces as $piece)
$builder->addPiece($piece);
return $builder;
}
}