|
|
|
|
@ -0,0 +1,388 @@
|
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Filesystem;
|
|
|
|
|
|
|
|
|
|
use GuzzleHttp\Client;
|
|
|
|
|
use GuzzleHttp\Exception\GuzzleException;
|
|
|
|
|
use League\Flysystem\Adapter\AbstractAdapter;
|
|
|
|
|
use League\Flysystem\Config;
|
|
|
|
|
|
|
|
|
|
class HttpGoAdapter extends AbstractAdapter
|
|
|
|
|
{
|
|
|
|
|
protected Client $client;
|
|
|
|
|
protected string $baseUri;
|
|
|
|
|
|
|
|
|
|
public function __construct(string $baseUri)
|
|
|
|
|
{
|
|
|
|
|
$this->baseUri = rtrim($baseUri, '/');
|
|
|
|
|
$this->client = new Client([
|
|
|
|
|
'base_uri' => $this->baseUri . '/',
|
|
|
|
|
'timeout' => 30,
|
|
|
|
|
'http_errors' => false,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 写入文件(base64 方式)
|
|
|
|
|
*/
|
|
|
|
|
public function write($path, $contents, Config $config): array|false
|
|
|
|
|
{
|
|
|
|
|
$fullPath = $this->applyPathPrefix($path);
|
|
|
|
|
$dir = dirname($fullPath);
|
|
|
|
|
$filename = basename($fullPath);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$response = $this->client->post('api/upload/base64', [
|
|
|
|
|
'json' => [
|
|
|
|
|
'file' => base64_encode($contents),
|
|
|
|
|
'path' => $dir,
|
|
|
|
|
'filename' => $filename,
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if ($response->getStatusCode() !== 200) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$body = json_decode($response->getBody(), true);
|
|
|
|
|
if (($body['code'] ?? 0) !== 200) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->normalizeMetadata($fullPath, $config);
|
|
|
|
|
} catch (GuzzleException $e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 写入文件流(multipart 方式)
|
|
|
|
|
*/
|
|
|
|
|
public function writeStream($path, $resource, Config $config): array|false
|
|
|
|
|
{
|
|
|
|
|
$fullPath = $this->applyPathPrefix($path);
|
|
|
|
|
$dir = dirname($fullPath);
|
|
|
|
|
$filename = basename($fullPath);
|
|
|
|
|
|
|
|
|
|
// 将流转换为临时文件
|
|
|
|
|
$tmpFile = tmpfile();
|
|
|
|
|
stream_copy_to_stream($resource, $tmpFile);
|
|
|
|
|
$meta = stream_get_meta_data($tmpFile);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$response = $this->client->post('api/upload/file', [
|
|
|
|
|
'multipart' => [
|
|
|
|
|
[
|
|
|
|
|
'name' => 'file',
|
|
|
|
|
'contents' => fopen($meta['uri'], 'r'),
|
|
|
|
|
'filename' => $filename,
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'name' => 'path',
|
|
|
|
|
'contents' => $dir,
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
fclose($tmpFile);
|
|
|
|
|
|
|
|
|
|
if ($response->getStatusCode() !== 200) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$body = json_decode($response->getBody(), true);
|
|
|
|
|
if (($body['code'] ?? 0) !== 200) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->normalizeMetadata($fullPath, $config);
|
|
|
|
|
} catch (GuzzleException $e) {
|
|
|
|
|
fclose($tmpFile);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新文件(委托给 write)
|
|
|
|
|
*/
|
|
|
|
|
public function update($path, $contents, Config $config): array|false
|
|
|
|
|
{
|
|
|
|
|
return $this->write($path, $contents, $config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新文件流(委托给 writeStream)
|
|
|
|
|
*/
|
|
|
|
|
public function updateStream($path, $resource, Config $config): array|false
|
|
|
|
|
{
|
|
|
|
|
return $this->writeStream($path, $resource, $config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 读取文件内容
|
|
|
|
|
*/
|
|
|
|
|
public function read($path): array|false
|
|
|
|
|
{
|
|
|
|
|
$fullPath = $this->applyPathPrefix($path);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$response = $this->client->get('files/' . ltrim($fullPath, '/'));
|
|
|
|
|
|
|
|
|
|
if ($response->getStatusCode() !== 200) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ['contents' => $response->getBody()->getContents()];
|
|
|
|
|
} catch (GuzzleException $e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 读取文件流
|
|
|
|
|
*/
|
|
|
|
|
public function readStream($path): array|false
|
|
|
|
|
{
|
|
|
|
|
$fullPath = $this->applyPathPrefix($path);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$response = $this->client->get('files/' . ltrim($fullPath, '/'), [
|
|
|
|
|
'stream' => true,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if ($response->getStatusCode() !== 200) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 返回一个临时流
|
|
|
|
|
$tmpFile = tmpfile();
|
|
|
|
|
$stream = $response->getBody()->detach();
|
|
|
|
|
if ($stream === null) {
|
|
|
|
|
fclose($tmpFile);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
stream_copy_to_stream($stream, $tmpFile);
|
|
|
|
|
fclose($stream);
|
|
|
|
|
rewind($tmpFile);
|
|
|
|
|
|
|
|
|
|
return ['stream' => $tmpFile];
|
|
|
|
|
} catch (GuzzleException $e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 检查文件是否存在
|
|
|
|
|
*/
|
|
|
|
|
public function has($path): bool
|
|
|
|
|
{
|
|
|
|
|
$fullPath = $this->applyPathPrefix($path);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$response = $this->client->head('files/' . ltrim($fullPath, '/'));
|
|
|
|
|
return $response->getStatusCode() === 200;
|
|
|
|
|
} catch (GuzzleException $e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 删除文件
|
|
|
|
|
*/
|
|
|
|
|
public function delete($path): bool
|
|
|
|
|
{
|
|
|
|
|
$fullPath = $this->applyPathPrefix($path);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$response = $this->client->delete('files/' . ltrim($fullPath, '/'));
|
|
|
|
|
return $response->getStatusCode() === 200;
|
|
|
|
|
} catch (GuzzleException $e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 删除目录
|
|
|
|
|
*/
|
|
|
|
|
public function deleteDir($dirname): bool
|
|
|
|
|
{
|
|
|
|
|
$fullPath = $this->applyPathPrefix($dirname);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$response = $this->client->delete('files/' . ltrim($fullPath, '/'));
|
|
|
|
|
return $response->getStatusCode() === 200;
|
|
|
|
|
} catch (GuzzleException $e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 创建目录(Go 服务隐式创建,直接返回成功)
|
|
|
|
|
*/
|
|
|
|
|
public function createDir($dirname, Config $config): array|false
|
|
|
|
|
{
|
|
|
|
|
return ['path' => $dirname, 'type' => 'dir'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 设置可见性(始终 public)
|
|
|
|
|
*/
|
|
|
|
|
public function setVisibility($path, $visibility): array|false
|
|
|
|
|
{
|
|
|
|
|
return ['visibility' => $visibility, 'path' => $path];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取可见性
|
|
|
|
|
*/
|
|
|
|
|
public function getVisibility($path): array|false
|
|
|
|
|
{
|
|
|
|
|
return ['visibility' => 'public', 'path' => $path];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取 MIME 类型
|
|
|
|
|
*/
|
|
|
|
|
public function getMimetype($path): array|false
|
|
|
|
|
{
|
|
|
|
|
$fullPath = $this->applyPathPrefix($path);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$response = $this->client->head('files/' . ltrim($fullPath, '/'));
|
|
|
|
|
|
|
|
|
|
if ($response->getStatusCode() !== 200) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$contentType = $response->getHeader('Content-Type')[0] ?? 'application/octet-stream';
|
|
|
|
|
return ['mimetype' => $contentType];
|
|
|
|
|
} catch (GuzzleException $e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取文件大小
|
|
|
|
|
*/
|
|
|
|
|
public function getSize($path): array|false
|
|
|
|
|
{
|
|
|
|
|
$fullPath = $this->applyPathPrefix($path);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$response = $this->client->head('files/' . ltrim($fullPath, '/'));
|
|
|
|
|
|
|
|
|
|
if ($response->getStatusCode() !== 200) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$size = (int)($response->getHeader('Content-Length')[0] ?? 0);
|
|
|
|
|
return ['size' => $size];
|
|
|
|
|
} catch (GuzzleException $e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取文件修改时间
|
|
|
|
|
*/
|
|
|
|
|
public function getTimestamp($path): array|false
|
|
|
|
|
{
|
|
|
|
|
$fullPath = $this->applyPathPrefix($path);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$response = $this->client->head('files/' . ltrim($fullPath, '/'));
|
|
|
|
|
|
|
|
|
|
if ($response->getStatusCode() !== 200) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$lastModified = $response->getHeader('Last-Modified')[0] ?? '';
|
|
|
|
|
$timestamp = $lastModified ? strtotime($lastModified) : time();
|
|
|
|
|
return ['timestamp' => $timestamp];
|
|
|
|
|
} catch (GuzzleException $e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取文件元信息
|
|
|
|
|
*/
|
|
|
|
|
public function getMetadata($path): array|false
|
|
|
|
|
{
|
|
|
|
|
$fullPath = $this->applyPathPrefix($path);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$response = $this->client->head('files/' . ltrim($fullPath, '/'));
|
|
|
|
|
|
|
|
|
|
if ($response->getStatusCode() !== 200) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$contentType = $response->getHeader('Content-Type')[0] ?? 'application/octet-stream';
|
|
|
|
|
$size = (int)($response->getHeader('Content-Length')[0] ?? 0);
|
|
|
|
|
$lastModified = $response->getHeader('Last-Modified')[0] ?? '';
|
|
|
|
|
$timestamp = $lastModified ? strtotime($lastModified) : time();
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'type' => 'file',
|
|
|
|
|
'path' => $fullPath,
|
|
|
|
|
'timestamp' => $timestamp,
|
|
|
|
|
'size' => $size,
|
|
|
|
|
'mimetype' => $contentType,
|
|
|
|
|
];
|
|
|
|
|
} catch (GuzzleException $e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 列出目录内容(简化实现,返回空数组)
|
|
|
|
|
*/
|
|
|
|
|
public function listContents($directory = '', $recursive = false): array
|
|
|
|
|
{
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 重命名文件
|
|
|
|
|
*/
|
|
|
|
|
public function rename($path, $newpath): bool
|
|
|
|
|
{
|
|
|
|
|
$contents = $this->read($path);
|
|
|
|
|
if ($contents === false) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($this->write($newpath, $contents['contents'], new Config()) === false) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->delete($path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 复制文件
|
|
|
|
|
*/
|
|
|
|
|
public function copy($path, $newpath): bool
|
|
|
|
|
{
|
|
|
|
|
$contents = $this->read($path);
|
|
|
|
|
if ($contents === false) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->write($newpath, $contents['contents'], new Config()) !== false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 标准化元数据
|
|
|
|
|
*/
|
|
|
|
|
protected function normalizeMetadata(string $path, Config $config): array
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
'type' => 'file',
|
|
|
|
|
'path' => $path,
|
|
|
|
|
'timestamp' => time(),
|
|
|
|
|
'size' => 0,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|