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; } }