1
0
Files
services_core/app/Services/File/StoredFileService.php
T
2026-07-27 16:24:09 +07:00

118 lines
4.5 KiB
PHP

<?php
namespace App\Services\File;
use App\Models\StoredFile;
use App\Models\User;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class StoredFileService
{
public function list(array $filters, User $user, ?int $tenantId): LengthAwarePaginator
{
return StoredFile::query()
->when(! $user->isMasterAdmin(), function ($query) use ($user, $tenantId) {
$query->where(function ($scope) use ($user, $tenantId) {
$scope->where('uploaded_by', $user->id)
->when($tenantId, fn ($tenantScope) => $tenantScope->orWhere('tenant_id', $tenantId));
});
})
->when($filters['search'] ?? null, function ($query, $search) {
$query->where('original_name', 'like', "%{$search}%");
})
->when($filters['category'] ?? null, fn ($query, $category) => $query->where('category', $category))
->when($filters['mime_type'] ?? null, fn ($query, $mimeType) => $query->where('mime_type', $mimeType))
->latest('id')
->paginate($filters['per_page'] ?? 10);
}
public function store(UploadedFile $upload, User $user, ?int $tenantId, string $category): StoredFile
{
$uuid = (string) Str::uuid();
$extension = strtolower($upload->getClientOriginalExtension() ?: $upload->extension() ?: 'bin');
$scope = $tenantId ? "tenants/{$tenantId}" : 'global';
$objectKey = "{$scope}/{$category}/".now()->format('Y/m')."/{$uuid}.{$extension}";
$disk = Storage::disk('minio');
$disk->putFileAs(dirname($objectKey), $upload, basename($objectKey), [
'visibility' => 'private',
'ContentType' => $upload->getMimeType() ?: 'application/octet-stream',
]);
try {
return DB::transaction(fn () => StoredFile::create([
'uuid' => $uuid,
'tenant_id' => $tenantId,
'uploaded_by' => $user->id,
'disk' => 'minio',
'bucket' => (string) config('filesystems.disks.minio.bucket'),
'object_key' => $objectKey,
'original_name' => $upload->getClientOriginalName(),
'extension' => $extension,
'mime_type' => $upload->getMimeType(),
'size' => $upload->getSize(),
'category' => $category,
'visibility' => 'private',
]));
} catch (\Throwable $exception) {
$disk->delete($objectKey);
throw $exception;
}
}
public function temporaryUrl(StoredFile $file, User $user): array
{
$this->authorizeAccess($file, $user);
$expiresAt = now()->addMinutes(10);
return [
'url' => Storage::disk($file->disk)->temporaryUrl(
$file->object_key,
$expiresAt,
['ResponseContentDisposition' => 'inline; filename="'.addslashes($file->original_name).'"']
),
'expires_at' => $expiresAt->toIso8601String(),
];
}
public function delete(StoredFile $file, User $user): void
{
if (! $user->isMasterAdmin() && $file->uploaded_by !== $user->id) {
abort(403, 'File hanya dapat dihapus oleh pengunggah atau Master Admin.');
}
Storage::disk($file->disk)->delete($file->object_key);
$file->delete();
}
public function authorizeAccess(StoredFile $file, User $user): void
{
if ($user->isMasterAdmin() || $file->uploaded_by === $user->id) {
return;
}
$canAccessTenant = $file->tenant_id && $user->userTenants()
->where('tenant_id', $file->tenant_id)
->exists();
abort_unless($canAccessTenant, 403, 'Anda tidak memiliki akses ke file ini.');
}
public function validateProfilePhoto(StoredFile $file, User $user): void
{
if ($file->uploaded_by !== $user->id
|| $file->category !== 'profile'
|| ! str_starts_with((string) $file->mime_type, 'image/')
|| $file->size > 5 * 1024 * 1024) {
throw ValidationException::withMessages([
'profile_photo_file_id' => 'Foto profil harus berupa file gambar kategori profile yang Anda unggah sendiri.',
]);
}
}
}