From cd4b7df1d7916561870a67df2969973c74ec9a86 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 4 Jan 2023 23:21:41 +0800 Subject: [PATCH] add onebot file downloader and uploader --- src/ZM/Utils/OneBot12FileDownloader.php | 129 ++++++++++++++++++++++++ src/ZM/Utils/OneBot12FileUploader.php | 128 +++++++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 src/ZM/Utils/OneBot12FileDownloader.php create mode 100644 src/ZM/Utils/OneBot12FileUploader.php diff --git a/src/ZM/Utils/OneBot12FileDownloader.php b/src/ZM/Utils/OneBot12FileDownloader.php new file mode 100644 index 00000000..f82eeb1a --- /dev/null +++ b/src/ZM/Utils/OneBot12FileDownloader.php @@ -0,0 +1,129 @@ +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; + } +} diff --git a/src/ZM/Utils/OneBot12FileUploader.php b/src/ZM/Utils/OneBot12FileUploader.php new file mode 100644 index 00000000..2663a8e8 --- /dev/null +++ b/src/ZM/Utils/OneBot12FileUploader.php @@ -0,0 +1,128 @@ +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; + } +}