teknolis / app /Services /HuggingFaceMediaStorageService.php
OpenAI Codex
Fix storage symlink and adapt cron status for Spaces
1501522
Raw
History Blame Contribute Delete
8.01 kB
<?php
namespace App\Services;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use RuntimeException;
class HuggingFaceMediaStorageService
{
public function offloadPublicRelativePath(string $relativePath): string
{
$cleanRelativePath = ltrim(trim($relativePath), '/');
if ($cleanRelativePath === '') {
throw new RuntimeException('Path media kosong, tidak bisa di-offload ke Hugging Face.');
}
if (! $this->isEnabled()) {
return Storage::url($cleanRelativePath);
}
$disk = Storage::disk('public');
if (! $disk->exists($cleanRelativePath)) {
return Storage::url($cleanRelativePath);
}
$token = $this->token();
$driver = $this->driver();
$repoId = $this->repoId();
$repoType = $this->repoType();
$bucketId = $this->bucketId();
if (
$token === ''
|| ($driver === 'repo' && $repoId === '')
|| ($driver === 'bucket' && $bucketId === '')
) {
Log::warning('HF media offload dilewati karena konfigurasi belum lengkap.', [
'driver' => $driver,
'has_token' => $token !== '',
'repo_id' => $repoId,
'bucket_id' => $bucketId,
]);
return Storage::url($cleanRelativePath);
}
$absolutePath = $disk->path($cleanRelativePath);
$pathInfo = pathinfo($cleanRelativePath);
$extension = strtolower((string) ($pathInfo['extension'] ?? ''));
$typeFolder = Str::startsWith($cleanRelativePath, 'videos/') ? 'videos' : 'images';
$dateFolder = now()->format('Y/m/d');
$filename = ($pathInfo['filename'] ?? 'asset').($extension !== '' ? '.'.$extension : '');
$remotePath = trim($this->remotePrefix().'/'.$typeFolder.'/'.$dateFolder.'/'.$filename, '/');
$hfBinary = $this->hfBinary();
$command = $driver === 'bucket'
? $this->buildBucketUploadCommand($hfBinary, $bucketId, $absolutePath, $remotePath, $token)
: $this->buildRepoUploadCommand($hfBinary, $repoId, $absolutePath, $remotePath, $repoType, $token);
$outputLines = [];
$exitCode = 1;
@exec($command.' 2>&1', $outputLines, $exitCode);
if ($exitCode !== 0) {
Log::error('HF media offload gagal.', [
'relative_path' => $cleanRelativePath,
'command' => $command,
'exit_code' => $exitCode,
'output' => implode("\n", $outputLines),
]);
return Storage::url($cleanRelativePath);
}
if ($this->deleteLocalAfterOffload() && $disk->exists($cleanRelativePath)) {
$disk->delete($cleanRelativePath);
}
return $driver === 'bucket'
? $this->resolveBucketUrl($bucketId, $remotePath)
: $this->resolveRepoUrl($repoType, $repoId, $remotePath);
}
private function buildRepoUploadCommand(
string $binary,
string $repoId,
string $absolutePath,
string $remotePath,
string $repoType,
string $token
): string {
$arguments = [
$binary,
'upload',
$repoId,
$absolutePath,
$remotePath,
'--repo-type',
$repoType,
'--token',
$token,
'--quiet',
];
return implode(' ', array_map(static fn (string $value): string => escapeshellarg($value), $arguments));
}
private function buildBucketUploadCommand(
string $binary,
string $bucketId,
string $absolutePath,
string $remotePath,
string $token
): string {
$bucketHandle = 'hf://buckets/'.trim($bucketId, '/').'/'.trim($remotePath, '/');
$arguments = [
$binary,
'buckets',
'cp',
$absolutePath,
$bucketHandle,
'--token',
$token,
'--quiet',
];
return implode(' ', array_map(static fn (string $value): string => escapeshellarg($value), $arguments));
}
private function resolveRepoUrl(string $repoType, string $repoId, string $remotePath): string
{
$encodedPath = implode('/', array_map('rawurlencode', explode('/', trim($remotePath, '/'))));
return match ($repoType) {
'dataset' => 'https://huggingface.co/datasets/'.$repoId.'/resolve/main/'.$encodedPath,
'space' => 'https://huggingface.co/spaces/'.$repoId.'/resolve/main/'.$encodedPath,
default => 'https://huggingface.co/'.$repoId.'/resolve/main/'.$encodedPath,
};
}
private function resolveBucketUrl(string $bucketId, string $remotePath): string
{
$encodedPath = rawurlencode(trim($remotePath, '/'));
return 'https://huggingface.co/buckets/'.trim($bucketId, '/').'/resolve/'.$encodedPath;
}
private function driver(): string
{
$value = strtolower(trim((string) (
config('services.huggingface.media_driver')
?? $this->readEnv('HF_MEDIA_DRIVER')
?? 'repo'
)));
return in_array($value, ['repo', 'bucket'], true) ? $value : 'repo';
}
private function isEnabled(): bool
{
$value = strtolower(trim((string) (
config('services.huggingface.media_enabled')
?? $this->readEnv('HF_MEDIA_ENABLED')
?? 'false'
)));
return in_array($value, ['1', 'true', 'yes', 'on'], true);
}
private function deleteLocalAfterOffload(): bool
{
$value = strtolower(trim((string) (
config('services.huggingface.media_delete_local')
?? $this->readEnv('HF_MEDIA_DELETE_LOCAL')
?? 'true'
)));
return in_array($value, ['1', 'true', 'yes', 'on'], true);
}
private function token(): string
{
return trim((string) (
config('services.huggingface.api_token')
?? $this->readEnv('HUGGINGFACE_API_TOKEN')
?? $this->readEnv('HF_API_TOKEN')
?? $this->readEnv('HF_TOKEN')
?? $this->readEnv('HUGGINGFACEHUB_API_TOKEN')
?? ''
));
}
private function repoId(): string
{
return trim((string) (
config('services.huggingface.media_repo_id')
?? $this->readEnv('HF_MEDIA_REPO_ID')
?? $this->readEnv('HUGGINGFACE_MEDIA_REPO_ID')
?? ''
));
}
private function bucketId(): string
{
return trim((string) (
config('services.huggingface.media_bucket_id')
?? $this->readEnv('HF_MEDIA_BUCKET_ID')
?? $this->readEnv('HUGGINGFACE_MEDIA_BUCKET_ID')
?? ''
));
}
private function repoType(): string
{
$value = strtolower(trim((string) (
config('services.huggingface.media_repo_type')
?? $this->readEnv('HF_MEDIA_REPO_TYPE')
?? 'dataset'
)));
return in_array($value, ['dataset', 'model', 'space'], true) ? $value : 'dataset';
}
private function remotePrefix(): string
{
return trim((string) (
config('services.huggingface.media_path_prefix')
?? $this->readEnv('HF_MEDIA_PATH_PREFIX')
?? 'laravel-media'
));
}
private function hfBinary(): string
{
return trim((string) (
config('services.huggingface.cli_binary')
?? $this->readEnv('HF_CLI_BINARY')
?? 'hf'
));
}
private function readEnv(string $key): ?string
{
$value = $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key);
if (! is_string($value)) {
return null;
}
$trimmed = trim($value);
return $trimmed === '' ? null : $trimmed;
}
}