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; } }