2025-05-12 00:19:35 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Service\common\upload;
|
|
|
|
|
|
|
2026-08-10 18:32:06 +08:00
|
|
|
|
use App\BaseApp\BaseNotAuthService;
|
|
|
|
|
|
use App\Service\common\oss\OssStorageInterface;
|
2025-05-12 00:19:35 +08:00
|
|
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
|
|
|
2026-08-10 18:32:06 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 本地磁盘上传;支持 domain / path_prefix 覆盖
|
|
|
|
|
|
*/
|
|
|
|
|
|
class LocalhostStorageService extends BaseNotAuthService implements OssStorageInterface
|
2025-05-12 00:19:35 +08:00
|
|
|
|
{
|
2026-08-14 23:21:21 +08:00
|
|
|
|
use ObjectKeyNormalizeTrait;
|
|
|
|
|
|
|
2026-08-10 18:32:06 +08:00
|
|
|
|
protected array $config = [
|
|
|
|
|
|
'domain' => '',
|
|
|
|
|
|
'path_prefix' => '',
|
|
|
|
|
|
];
|
2025-05-12 00:19:35 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
2026-08-10 18:32:06 +08:00
|
|
|
|
* 注入运行时配置(链式)
|
2025-05-12 00:19:35 +08:00
|
|
|
|
*/
|
2026-08-10 18:32:06 +08:00
|
|
|
|
public function withConfig(array $config): static
|
2025-05-12 00:19:35 +08:00
|
|
|
|
{
|
2026-08-10 18:32:06 +08:00
|
|
|
|
$this->config = array_merge($this->config, $config);
|
|
|
|
|
|
return $this;
|
2025-05-12 00:19:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 18:32:06 +08:00
|
|
|
|
public function uploadImage($filePath, string $key): bool|array
|
2025-05-12 00:19:35 +08:00
|
|
|
|
{
|
|
|
|
|
|
return $this->uploadFile($filePath, $key);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 18:32:06 +08:00
|
|
|
|
public function uploadVideo($filePath, string $key): bool|array
|
2025-05-12 00:19:35 +08:00
|
|
|
|
{
|
|
|
|
|
|
return $this->uploadFile($filePath, $key);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function deleteImage($key): bool
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->deleteFile($key);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function deleteVideo($key): bool
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->deleteFile($key);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 23:21:21 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 扫本地磁盘目录
|
|
|
|
|
|
*
|
|
|
|
|
|
* 本地盘没有 marker 这种服务端游标,用「已列举条数」当偏移量模拟:
|
|
|
|
|
|
* 先把文件名排序固定顺序,再按偏移切页,这样反复调用能稳定推进。
|
|
|
|
|
|
* 代价是同步期间新增文件会让偏移错位,但素材同步是幂等的,下一轮就自愈。
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function listObjects(string $prefix = '', string $marker = '', int $limit = 100): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$listPrefix = rtrim($this->scopedListPrefix($prefix), '/');
|
|
|
|
|
|
$offset = max(0, (int) $marker);
|
|
|
|
|
|
$files = Storage::allFiles($listPrefix);
|
|
|
|
|
|
sort($files);
|
|
|
|
|
|
$page = array_slice($files, $offset, $this->boundedLimit($limit));
|
|
|
|
|
|
|
|
|
|
|
|
$items = [];
|
|
|
|
|
|
foreach ($page as $file) {
|
|
|
|
|
|
$key = $this->normalizeObjectKey($file);
|
|
|
|
|
|
if ($key === '') {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
$realPath = Storage::path($file);
|
|
|
|
|
|
$items[] = [
|
|
|
|
|
|
'key' => $key,
|
|
|
|
|
|
'size' => (int) Storage::size($file),
|
|
|
|
|
|
// 本地盘没有服务端 ETag,用 md5 顶上,素材库靠它判重与校验
|
|
|
|
|
|
'hash' => is_file($realPath) ? (string) md5_file($realPath) : '',
|
|
|
|
|
|
'last_modified' => (int) Storage::lastModified($file),
|
|
|
|
|
|
'url' => $this->publicUrlOf($key, asset('/storage/' . $key)),
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$next = $offset + count($page);
|
|
|
|
|
|
$finished = $next >= count($files);
|
|
|
|
|
|
return [
|
|
|
|
|
|
'items' => $items,
|
|
|
|
|
|
'next_marker' => $finished ? '' : (string) $next,
|
|
|
|
|
|
'finished' => $finished,
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function deleteObject(string $key): bool
|
|
|
|
|
|
{
|
|
|
|
|
|
$key = $this->normalizeObjectKey($key);
|
|
|
|
|
|
if ($key === '') {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return $this->deleteFile($key);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-12 00:19:35 +08:00
|
|
|
|
/**
|
2026-08-10 18:32:06 +08:00
|
|
|
|
* 写入本地 storage;兼容 UploadedFile 与路径字符串
|
2025-05-12 00:19:35 +08:00
|
|
|
|
*/
|
2026-08-10 18:32:06 +08:00
|
|
|
|
private function uploadFile(mixed $filePath, string $key): bool|array
|
2025-05-12 00:19:35 +08:00
|
|
|
|
{
|
2026-08-10 18:32:06 +08:00
|
|
|
|
$prefix = trim((string) ($this->config['path_prefix'] ?? ''), '/');
|
|
|
|
|
|
if ($prefix !== '' && !str_starts_with($key, $prefix . '/')) {
|
|
|
|
|
|
$key = $prefix . '/' . ltrim($key, '/');
|
|
|
|
|
|
}
|
|
|
|
|
|
$path = is_object($filePath) && method_exists($filePath, 'getRealPath')
|
|
|
|
|
|
? $filePath->getRealPath()
|
|
|
|
|
|
: (string) $filePath;
|
|
|
|
|
|
if (!Storage::put($key, file_get_contents($path))) {
|
|
|
|
|
|
return false;
|
2025-05-12 00:19:35 +08:00
|
|
|
|
}
|
2026-08-10 18:32:06 +08:00
|
|
|
|
$domain = rtrim((string) ($this->config['domain'] ?? ''), '/');
|
|
|
|
|
|
$url = $domain !== '' ? ($domain . '/' . $key) : asset('/storage/' . $key);
|
|
|
|
|
|
return [
|
|
|
|
|
|
'key' => $key,
|
|
|
|
|
|
'url' => $url,
|
|
|
|
|
|
];
|
2025-05-12 00:19:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private function deleteFile(string $key): bool
|
|
|
|
|
|
{
|
|
|
|
|
|
return Storage::delete($key);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|