mirror of
https://github.com/zhamao-robot/zhamao-framework.git
synced 2026-03-17 20:54:52 +08:00
add onebot file downloader and uploader
This commit is contained in:
parent
7c30ba6b03
commit
cd4b7df1d7
129
src/ZM/Utils/OneBot12FileDownloader.php
Normal file
129
src/ZM/Utils/OneBot12FileDownloader.php
Normal file
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Utils;
|
||||
|
||||
use OneBot\V12\Object\ActionResponse;
|
||||
use ZM\Context\BotContext;
|
||||
use ZM\Store\FileSystem;
|
||||
|
||||
/**
|
||||
* 一个支持 OneBot12 标准的文件传输器之下载部分
|
||||
*/
|
||||
class OneBot12FileDownloader
|
||||
{
|
||||
/** @var string 下载路径 */
|
||||
private string $download_path;
|
||||
|
||||
/** @var string 错误信息 */
|
||||
private string $err = '';
|
||||
|
||||
public function __construct(private BotContext $ctx, private int $buffer_size = 524288)
|
||||
{
|
||||
$this->setDownloadPath(zm_dir(config('global.data_dir') . '/files'));
|
||||
}
|
||||
|
||||
public function setDownloadPath(string $download_path): bool
|
||||
{
|
||||
$this->download_path = $download_path;
|
||||
try {
|
||||
FileSystem::createDir($this->download_path);
|
||||
return true;
|
||||
} catch (\RuntimeException $e) {
|
||||
$this->err = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function downloadFile(string $file_id, bool $fragmented = true): bool|string
|
||||
{
|
||||
if ($this->err !== '') {
|
||||
return false;
|
||||
}
|
||||
logger()->info('Downloading file ' . $file_id);
|
||||
if (!$fragmented) {
|
||||
$obj = $this->ctx->sendAction('get_file', [
|
||||
'file_id' => $file_id,
|
||||
'type' => 'data',
|
||||
]);
|
||||
if (!$obj instanceof ActionResponse) {
|
||||
$this->err = 'coroutine not enabled, cannot receive action response';
|
||||
return false;
|
||||
}
|
||||
if ($obj->retcode !== 0) {
|
||||
$this->err = 'get file failed with code ' . $obj->retcode . ' : ' . $obj->message;
|
||||
return false;
|
||||
}
|
||||
$name = $obj->data['name'];
|
||||
$data = base64_decode($obj->data['data']);
|
||||
// TODO: Walle-Q 返回的 sha256 是空的
|
||||
/* if ($obj->data['sha256'] !== hash('sha256', $data)) {
|
||||
$this->err = 'sha256 mismatch between ' . $obj->data['sha256'] . ' and ' . hash('sha256', $data) . "\n" . json_encode($obj);
|
||||
return false;
|
||||
}*/
|
||||
} else {
|
||||
$obj = $this->ctx->sendAction('get_file_fragmented', [
|
||||
'stage' => 'prepare',
|
||||
'file_id' => $file_id,
|
||||
]);
|
||||
if (!$obj instanceof ActionResponse) {
|
||||
$this->err = 'coroutine not enabled, cannot receive action response';
|
||||
return false;
|
||||
}
|
||||
if ($obj->retcode !== 0) {
|
||||
$this->err = 'get file fragment failed with code ' . $obj->retcode . ' : ' . $obj->message;
|
||||
return false;
|
||||
}
|
||||
$name = $obj->data['name'];
|
||||
$data = '';
|
||||
$total_size = $obj->data['total_size'];
|
||||
$sha256 = $obj->data['sha256'];
|
||||
$slice = intval($total_size / $this->buffer_size) + ($total_size % $this->buffer_size > 0 ? 1 : 0);
|
||||
for ($i = 0; $i < $slice; ++$i) {
|
||||
$trans = $this->ctx->sendAction('get_file_fragmented', [
|
||||
'stage' => 'transfer',
|
||||
'file_id' => $file_id,
|
||||
'offset' => $i * $this->buffer_size,
|
||||
'size' => $this->buffer_size,
|
||||
]);
|
||||
if (!$trans instanceof ActionResponse) {
|
||||
$this->err = 'coroutine not enabled, cannot receive action response';
|
||||
return false;
|
||||
}
|
||||
if ($trans->retcode !== 0) {
|
||||
$this->err = 'get file fragment failed with code ' . $trans->retcode . ' : ' . $trans->message;
|
||||
return false;
|
||||
}
|
||||
$data .= base64_decode($trans->data['data']);
|
||||
}
|
||||
// TODO: Walle-Q 返回的 sha256 是空的
|
||||
/* if ($sha256 !== hash('sha256', $data)) {
|
||||
$this->err = 'sha256 mismatch';
|
||||
return false;
|
||||
}*/
|
||||
}
|
||||
if (str_contains($name, '..')) {
|
||||
$this->err = 'invalid file name';
|
||||
return false;
|
||||
}
|
||||
$path = zm_dir($this->download_path . '/' . $name);
|
||||
if (file_exists($path)) {
|
||||
$this->err = 'file already exists';
|
||||
return false;
|
||||
}
|
||||
if (!file_put_contents($path, $data)) {
|
||||
$this->err = 'failed to write file';
|
||||
return false;
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误信息
|
||||
*/
|
||||
public function getErr(): string
|
||||
{
|
||||
return $this->err;
|
||||
}
|
||||
}
|
||||
128
src/ZM/Utils/OneBot12FileUploader.php
Normal file
128
src/ZM/Utils/OneBot12FileUploader.php
Normal file
@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Utils;
|
||||
|
||||
use OneBot\V12\Object\ActionResponse;
|
||||
use ZM\Context\BotContext;
|
||||
|
||||
/**
|
||||
* 一个支持 OneBot12 标准的文件传输器之上传部分
|
||||
*/
|
||||
class OneBot12FileUploader
|
||||
{
|
||||
private string $err = '';
|
||||
|
||||
/**
|
||||
* 构建一个文件上传器
|
||||
*
|
||||
* @param BotContext $ctx 机器人上下文,用于调用发送动作
|
||||
* @param int $buffer_size 分片传输的大小,默认为 65536 字节,建议调整小于 2MB
|
||||
*/
|
||||
public function __construct(private BotContext $ctx, private int $buffer_size = 1048576)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过文件内容上传一个文件
|
||||
* 当上传失败时返回 False,上传成功则返回 file_id
|
||||
*
|
||||
* @param string $filename 要上传的文件名
|
||||
* @param string $content 要上传的文件内容
|
||||
* @return bool|string 上传失败返回 false,可通过 getErr() 获取错误信息。上传成功返回 file_id
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function uploadFromString(string $filename, string $content): bool|string
|
||||
{
|
||||
logger()->info('Uploading file, size: ' . strlen($content));
|
||||
|
||||
$size = strlen($content);
|
||||
$offset = 0;
|
||||
// 文件本身小于分片大小,直接一个包发送
|
||||
if ($size <= $this->buffer_size) {
|
||||
$obj = $this->ctx->sendAction('upload_file', [
|
||||
'type' => 'data',
|
||||
'name' => $filename,
|
||||
'data' => base64_encode($content),
|
||||
'sha256' => hash('sha256', $content),
|
||||
]);
|
||||
if (!$obj instanceof ActionResponse) {
|
||||
$this->err = 'prepare stage returns an non-response object';
|
||||
return false;
|
||||
}
|
||||
if ($obj->retcode !== 0) {
|
||||
$this->err = 'prepare stage returns an error: ' . $obj->retcode;
|
||||
return false;
|
||||
}
|
||||
return $obj->data['file_id'];
|
||||
}
|
||||
// 其他情况,使用分片的方式发送,依次调用 prepare, transfer, finish
|
||||
$obj = $this->ctx->sendAction('upload_file_fragmented', [
|
||||
'stage' => 'prepare',
|
||||
'name' => $filename,
|
||||
'total_size' => strlen($content),
|
||||
]);
|
||||
if (!$obj instanceof ActionResponse) {
|
||||
$this->err = 'prepare stage returns an non-response object';
|
||||
return false;
|
||||
}
|
||||
if ($obj->retcode !== 0) {
|
||||
$this->err = 'prepare stage returns an error: ' . $obj->retcode;
|
||||
return false;
|
||||
}
|
||||
$file_id = $obj->data['file_id'];
|
||||
while ($offset < $size) {
|
||||
$data = substr($content, $offset, $this->buffer_size);
|
||||
$this->ctx->sendAction('upload_file_fragmented', [
|
||||
'stage' => 'transfer',
|
||||
'file_id' => $file_id,
|
||||
'offset' => $offset,
|
||||
'data' => base64_encode($data),
|
||||
]);
|
||||
if (strlen($data) < $this->buffer_size) {
|
||||
break;
|
||||
}
|
||||
$offset += $this->buffer_size;
|
||||
}
|
||||
|
||||
$final_obj = $this->ctx->sendAction('upload_file_fragmented', [
|
||||
'stage' => 'finish',
|
||||
'file_id' => $file_id,
|
||||
'sha256' => hash('sha256', $content),
|
||||
]);
|
||||
if (!$final_obj instanceof ActionResponse) {
|
||||
$this->err = 'finish error with non-object';
|
||||
return false;
|
||||
}
|
||||
if ($final_obj->retcode !== 0) {
|
||||
$this->err = 'finish error: ' . $final_obj->retcode;
|
||||
return false;
|
||||
}
|
||||
return $final_obj->data['file_id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地路径上传一个文件
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function uploadFromPath(string $file_path): bool|string
|
||||
{
|
||||
if (file_exists($file_path)) {
|
||||
$name = pathinfo($file_path, PATHINFO_BASENAME);
|
||||
$content = file_get_contents($file_path);
|
||||
return $this->uploadFromString($name, $content);
|
||||
}
|
||||
$this->err = 'File from path ' . $file_path . ' does not exist.';
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误消息
|
||||
*/
|
||||
public function getErr(): string
|
||||
{
|
||||
return $this->err;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user