337 lines
13 KiB
PHP
337 lines
13 KiB
PHP
<?php
|
|
|
|
namespace danog\MadelineProto\MTProtoTools;
|
|
|
|
use Amp\ByteStream\InputStream;
|
|
use Amp\ByteStream\IteratorStream;
|
|
use Amp\ByteStream\OutputStream;
|
|
use Amp\ByteStream\ResourceInputStream;
|
|
use Amp\ByteStream\ResourceOutputStream;
|
|
use Amp\ByteStream\StreamException;
|
|
use Amp\File\BlockingFile;
|
|
|
|
use Amp\File\Handle;
|
|
use Amp\File\StatCache as StatCacheAsync;
|
|
use Amp\Http\Client\Request;
|
|
use Amp\Http\Server\Request as ServerRequest;
|
|
use Amp\Http\Server\Response;
|
|
use Amp\Http\Status;
|
|
use Amp\Producer;
|
|
use danog\MadelineProto\Exception;
|
|
use danog\MadelineProto\FileCallbackInterface;
|
|
use danog\MadelineProto\Stream\Common\BufferedRawStream;
|
|
use danog\MadelineProto\Stream\Common\SimpleBufferedRawStream;
|
|
use danog\MadelineProto\Stream\ConnectionContext;
|
|
use danog\MadelineProto\Stream\Transport\PremadeStream;
|
|
use danog\MadelineProto\Tools;
|
|
|
|
|
|
use function Amp\File\exists;
|
|
use function Amp\File\open;
|
|
use function Amp\File\stat as statAsync;
|
|
|
|
trait FilesLogic
|
|
{
|
|
/**
|
|
* Download file to browser.
|
|
*
|
|
* Supports HEAD requests and content-ranges for parallel and resumed downloads.
|
|
*
|
|
* @param array|string $messageMedia File to download
|
|
* @param callable $cb Status callback (can also use FileCallback)
|
|
*
|
|
* @return \Generator
|
|
*/
|
|
public function downloadToBrowser($messageMedia, callable $cb = null): \Generator
|
|
{
|
|
if (\is_object($messageMedia) && $messageMedia instanceof FileCallbackInterface) {
|
|
$cb = $messageMedia;
|
|
$messageMedia = $messageMedia->getFile();
|
|
}
|
|
|
|
$headers = [];
|
|
if (isset($_SERVER['HTTP_RANGE'])) {
|
|
$headers['range'] = $_SERVER['HTTP_RANGE'];
|
|
}
|
|
|
|
$messageMedia = yield from $this->getDownloadInfo($messageMedia);
|
|
$result = ResponseInfo::parseHeaders(
|
|
$_SERVER['REQUEST_METHOD'],
|
|
$headers,
|
|
$messageMedia
|
|
);
|
|
|
|
foreach ($result->getHeaders() as $key => $value) {
|
|
if (\is_array($value)) {
|
|
foreach ($value as $subValue) {
|
|
\header("$key: $subValue", false);
|
|
}
|
|
} else {
|
|
\header("$key: $value");
|
|
}
|
|
}
|
|
\http_response_code($result->getCode());
|
|
|
|
if (!\in_array($result->getCode(), [Status::OK, Status::PARTIAL_CONTENT])) {
|
|
yield Tools::echo($result->getCodeExplanation());
|
|
} elseif ($result->shouldServe()) {
|
|
if (\ob_get_level()) {
|
|
\ob_end_flush();
|
|
\ob_implicit_flush();
|
|
}
|
|
yield from $this->downloadToStream($messageMedia, \fopen('php://output', 'w'), $cb, ...$result->getServeRange());
|
|
}
|
|
}
|
|
/**
|
|
* Download file to stream.
|
|
*
|
|
* @param mixed $messageMedia File to download
|
|
* @param mixed|FileCallbackInterface $stream Stream where to download file
|
|
* @param callable $cb Callback (DEPRECATED, use FileCallbackInterface)
|
|
* @param int $offset Offset where to start downloading
|
|
* @param int $end Offset where to end download
|
|
*
|
|
* @return \Generator
|
|
*
|
|
* @psalm-return \Generator<int, \Amp\Promise<\Amp\Ipc\Sync\ChannelledSocket>|\Amp\Promise<mixed>|mixed, mixed, mixed>
|
|
*/
|
|
public function downloadToStream($messageMedia, $stream, $cb = null, int $offset = 0, int $end = -1): \Generator
|
|
{
|
|
$messageMedia = yield from $this->getDownloadInfo($messageMedia);
|
|
if (\is_object($stream) && $stream instanceof FileCallbackInterface) {
|
|
$cb = $stream;
|
|
$stream = $stream->getFile();
|
|
}
|
|
/** @var $stream \Amp\ByteStream\OutputStream */
|
|
if (!\is_object($stream)) {
|
|
$stream = new ResourceOutputStream($stream);
|
|
}
|
|
if (!$stream instanceof OutputStream) {
|
|
throw new Exception("Invalid stream provided");
|
|
}
|
|
$seekable = false;
|
|
if (\method_exists($stream, 'seek')) {
|
|
try {
|
|
yield $stream->seek($offset);
|
|
$seekable = true;
|
|
} catch (StreamException $e) {
|
|
}
|
|
}
|
|
$callable = static function (string $payload, int $offset) use ($stream, $seekable): \Generator {
|
|
if ($seekable) {
|
|
while ($stream->tell() !== $offset) {
|
|
yield $stream->seek($offset);
|
|
}
|
|
}
|
|
return yield $stream->write($payload);
|
|
};
|
|
return yield from $this->downloadToCallable($messageMedia, $callable, $cb, $seekable, $offset, $end);
|
|
}
|
|
|
|
/**
|
|
* Download file to amphp/http-server response.
|
|
*
|
|
* Supports HEAD requests and content-ranges for parallel and resumed downloads.
|
|
*
|
|
* @param array|string $messageMedia File to download
|
|
* @param ServerRequest $request Request
|
|
* @param callable $cb Status callback (can also use FileCallback)
|
|
*
|
|
* @return \Generator<Response> Returned response
|
|
*/
|
|
public function downloadToResponse($messageMedia, ServerRequest $request, callable $cb = null): \Generator
|
|
{
|
|
if (\is_object($messageMedia) && $messageMedia instanceof FileCallbackInterface) {
|
|
$cb = $messageMedia;
|
|
$messageMedia = $messageMedia->getFile();
|
|
}
|
|
|
|
$messageMedia = yield from $this->getDownloadInfo($messageMedia);
|
|
|
|
$result = ResponseInfo::parseHeaders(
|
|
$request->getMethod(),
|
|
\array_map(fn (array $headers) => $headers[0], $request->getHeaders()),
|
|
$messageMedia
|
|
);
|
|
|
|
$body = null;
|
|
if ($result->shouldServe()) {
|
|
$body = new IteratorStream(
|
|
new Producer(
|
|
function (callable $emit) use (&$messageMedia, &$cb, &$result) {
|
|
$emit = static function (string $payload) use ($emit): \Generator {
|
|
yield $emit($payload);
|
|
return \strlen($payload);
|
|
};
|
|
yield Tools::call($this->downloadToCallable($messageMedia, $emit, $cb, false, ...$result->getServeRange()));
|
|
}
|
|
)
|
|
);
|
|
} elseif (!\in_array($result->getCode(), [Status::OK, Status::PARTIAL_CONTENT])) {
|
|
$body = $result->getCodeExplanation();
|
|
}
|
|
|
|
$response = new Response($result->getCode(), $result->getHeaders(), $body);
|
|
if ($result->shouldServe() && !empty($result->getHeaders()['Content-Length'])) {
|
|
$response->setHeader('content-length', $result->getHeaders()['Content-Length']);
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* Upload file to secret chat.
|
|
*
|
|
* @param FileCallbackInterface|string|array $file File, URL or Telegram file to upload
|
|
* @param string $fileName File name
|
|
* @param callable $cb Callback (DEPRECATED, use FileCallbackInterface)
|
|
*
|
|
* @return \Generator
|
|
*
|
|
* @psalm-return \Generator<int|mixed, \Amp\Promise|\Amp\Promise<\Amp\File\File>|array, mixed, mixed>
|
|
*/
|
|
public function uploadEncrypted($file, string $fileName = '', $cb = null): \Generator
|
|
{
|
|
return $this->upload($file, $fileName, $cb, true);
|
|
}
|
|
|
|
/**
|
|
* Upload file.
|
|
*
|
|
* @param FileCallbackInterface|string|array $file File, URL or Telegram file to upload
|
|
* @param string $fileName File name
|
|
* @param callable $cb Callback (DEPRECATED, use FileCallbackInterface)
|
|
* @param boolean $encrypted Whether to encrypt file for secret chats
|
|
*
|
|
* @return \Generator
|
|
*
|
|
* @psalm-return \Generator<int|mixed, \Amp\Promise|\Amp\Promise<\Amp\File\File>|\Amp\Promise<\Amp\Ipc\Sync\ChannelledSocket>|\Amp\Promise<int>|\Amp\Promise<mixed>|\Amp\Promise<null|string>|\danog\MadelineProto\Stream\StreamInterface|array|int|mixed, mixed, mixed>
|
|
*/
|
|
public function upload($file, string $fileName = '', $cb = null, bool $encrypted = false): \Generator
|
|
{
|
|
if (\is_object($file) && $file instanceof FileCallbackInterface) {
|
|
$cb = $file;
|
|
$file = $file->getFile();
|
|
}
|
|
if (\is_string($file) || \is_object($file) && \method_exists($file, '__toString')) {
|
|
if (\filter_var($file, FILTER_VALIDATE_URL)) {
|
|
return yield from $this->uploadFromUrl($file, 0, $fileName, $cb, $encrypted);
|
|
}
|
|
} elseif (\is_array($file)) {
|
|
return yield from $this->uploadFromTgfile($file, $cb, $encrypted);
|
|
}
|
|
if (\is_resource($file) || (\is_object($file) && $file instanceof InputStream)) {
|
|
return yield from $this->uploadFromStream($file, 0, '', $fileName, $cb, $encrypted);
|
|
}
|
|
if (!$this->settings->getFiles()->getAllowAutomaticUpload()) {
|
|
return yield from $this->uploadFromUrl($file, 0, $fileName, $cb, $encrypted);
|
|
}
|
|
$file = Tools::absolute($file);
|
|
if (!yield exists($file)) {
|
|
throw new \danog\MadelineProto\Exception(\danog\MadelineProto\Lang::$current_lang['file_not_exist']);
|
|
}
|
|
if (empty($fileName)) {
|
|
$fileName = \basename($file);
|
|
}
|
|
StatCacheAsync::clear($file);
|
|
$size = (yield statAsync($file))['size'];
|
|
if ($size > 512 * 1024 * 4000) {
|
|
throw new \danog\MadelineProto\Exception('Given file is too big!');
|
|
}
|
|
$stream = yield open($file, 'rb');
|
|
$mime = $this->getMimeFromFile($file);
|
|
try {
|
|
return yield from $this->uploadFromStream($stream, $size, $mime, $fileName, $cb, $encrypted);
|
|
} finally {
|
|
yield $stream->close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Upload file from stream.
|
|
*
|
|
* @param mixed $stream PHP resource or AMPHP async stream
|
|
* @param integer $size File size
|
|
* @param string $mime Mime type
|
|
* @param string $fileName File name
|
|
* @param callable $cb Callback (DEPRECATED, use FileCallbackInterface)
|
|
* @param boolean $encrypted Whether to encrypt file for secret chats
|
|
*
|
|
* @return \Generator
|
|
*
|
|
* @psalm-return \Generator<int|mixed, \Amp\Promise|\Amp\Promise<int>|\Amp\Promise<null|string>|\danog\MadelineProto\Stream\StreamInterface|array|int|mixed, mixed, mixed>
|
|
*/
|
|
public function uploadFromStream($stream, int $size, string $mime, string $fileName = '', $cb = null, bool $encrypted = false): \Generator
|
|
{
|
|
if (\is_object($stream) && $stream instanceof FileCallbackInterface) {
|
|
$cb = $stream;
|
|
$stream = $stream->getFile();
|
|
}
|
|
/* @var $stream \Amp\ByteStream\OutputStream */
|
|
if (!\is_object($stream)) {
|
|
$stream = new ResourceInputStream($stream);
|
|
}
|
|
if (!$stream instanceof InputStream) {
|
|
throw new Exception("Invalid stream provided");
|
|
}
|
|
$seekable = false;
|
|
if (\method_exists($stream, 'seek')) {
|
|
try {
|
|
yield $stream->seek(0);
|
|
$seekable = true;
|
|
} catch (StreamException $e) {
|
|
}
|
|
}
|
|
$created = false;
|
|
if ($stream instanceof Handle) {
|
|
$callable = static function (int $offset, int $size) use ($stream, $seekable): \Generator {
|
|
if ($seekable) {
|
|
while ($stream->tell() !== $offset) {
|
|
yield $stream->seek($offset);
|
|
}
|
|
}
|
|
return yield $stream->read($size);
|
|
};
|
|
} else {
|
|
if (!$stream instanceof BufferedRawStream) {
|
|
$ctx = (new ConnectionContext())->addStream(PremadeStream::class, $stream)->addStream(SimpleBufferedRawStream::class);
|
|
$stream = (yield from $ctx->getStream());
|
|
$created = true;
|
|
}
|
|
$callable = static function (int $offset, int $size) use ($stream): \Generator {
|
|
$reader = yield $stream->getReadBuffer($l);
|
|
try {
|
|
return yield $reader->bufferRead($size);
|
|
} catch (\danog\MadelineProto\NothingInTheSocketException $e) {
|
|
$reader = yield $stream->getReadBuffer($size);
|
|
return yield $reader->bufferRead($size);
|
|
}
|
|
};
|
|
$seekable = false;
|
|
}
|
|
if (!$size && $seekable && \method_exists($stream, 'tell')) {
|
|
yield $stream->seek(0, \SEEK_END);
|
|
$size = yield $stream->tell();
|
|
yield $stream->seek(0);
|
|
} elseif (!$size) {
|
|
$this->logger->logger("No content length for stream, caching first");
|
|
$body = $stream;
|
|
$stream = new BlockingFile(\fopen('php://temp', 'r+b'), 'php://temp', 'r+b');
|
|
while (null !== ($chunk = yield $body->read())) {
|
|
yield $stream->write($chunk);
|
|
}
|
|
$size = $stream->tell();
|
|
if (!$size) {
|
|
throw new Exception('Wrong size!');
|
|
}
|
|
yield $stream->seek(0);
|
|
return yield from $this->uploadFromStream($stream, $size, $mime, $fileName, $cb, $encrypted);
|
|
}
|
|
$res = (yield from $this->uploadFromCallable($callable, $size, $mime, $fileName, $cb, $seekable, $encrypted));
|
|
if ($created) {
|
|
$stream->disconnect();
|
|
}
|
|
return $res;
|
|
}
|
|
}
|