Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions Libs/Utils/FileUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
**/

use App\Http\Utils\FileUploadInfo;
use App\Services\Model\FileInfoDTO;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use models\exceptions\ValidationException;
Expand Down Expand Up @@ -139,6 +141,62 @@ public static function cleanLocalAndRemoteFile(string $localPath, string $remote
unlink($localPath);
}

/**
* Downloads a file from remote storage to a local temp path, verifies its MD5 (when provided),
* invokes $uploader($owner_entity_id, UploadedFile) to persist it, then cleans up. On failure the
* remote file is preserved so queue retries can re-download it. Cleanup errors after a successful
* upload are logged but not re-thrown - upload success determines job success, not storage housekeeping.
* @param FileInfoDTO $file_info_dto
* @param callable $uploader
* @return mixed whatever $uploader returns
* @throws ValidationException
*/
public static function processFileFromRemoteStorage(FileInfoDTO $file_info_dto, callable $uploader)
{
$localPath = self::getFileFromRemoteStorageOnTempStorage(
$file_info_dto->filename,
$file_info_dto->filepath
);
$succeeded = false;
try {
if (!is_null($file_info_dto->md5)) {
$localHash = md5_file($localPath);
if ($localHash === false)
throw new ValidationException("File integrity check failed: unable to read local temp file.");
if ($localHash !== strtolower($file_info_dto->md5))
throw new ValidationException("File integrity check failed: MD5 mismatch.");
}
$file = new UploadedFile(
path: $localPath,
originalName: $file_info_dto->filename,
mimeType: $file_info_dto->mime_type,
error: null,
test: true,
);
$res = $uploader($file_info_dto->owner_entity_id, $file);
$succeeded = true;
} finally {
if ($succeeded) {
try {
self::cleanLocalAndRemoteFile($localPath, $file_info_dto->filepath);
} catch (\Throwable $e) {
// Upload succeeded; cleanup failure is non-fatal. Log and continue so the
// job does not retry and create duplicate File records.
Log::warning(sprintf(
"FileUtils::processFileFromRemoteStorage cleanup failed after successful upload (entity=%s member=%s filepath=%s): %s",
$file_info_dto->owner_entity_class,
$file_info_dto->owner_member_name,
$file_info_dto->filepath,
$e->getMessage()
));
}
} else {
self::cleanLocalFile($localPath);
}
}
return $res;
}

/**
* Deletes only the local temp file. Use in finally blocks where the remote
* file must be preserved on failure so queue job retries can re-download it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ public static function build(array $data, $update = false){
'name' => 'sometimes|string',
'description' => 'sometimes|string',
'template_content' => 'nullable|string',
'image' => 'sometimes|file_dto',
];
}
return [
'name' => 'required|string',
'description' => 'sometimes|string',
'template_content' => 'nullable|string',
'image' => 'sometimes|file_dto',
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ class SummitBadgeFeatureType extends SilverstripeBaseModel
{
use SummitOwned;

public const ImageAllowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'svg'];

public const ImageMaxFileSize = 10485760; // bytes

/**
* @var string
*/
Expand Down
4 changes: 4 additions & 0 deletions app/Services/FilePostProcessorService.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
use App\Services\Model\ICompanyService;
use App\Services\Model\IFilePostProcessorForChildEntity;
use App\Services\Model\IFilePostProcessorService;
use App\Services\Model\ISummitBadgeFeatureTypeService;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use models\main\Company;
use models\summit\SummitBadgeFeatureType;
use models\utils\IEntity;

final class FilePostProcessorService implements IFilePostProcessorService
Expand All @@ -33,6 +35,8 @@ private function locateService(string $className): ?IFilePostProcessorForChildEn
switch($className){
case Company::class:
return App::make(ICompanyService::class);
case SummitBadgeFeatureType::class:
return App::make(ISummitBadgeFeatureTypeService::class);
}
return null;
}
Expand Down
4 changes: 2 additions & 2 deletions app/Services/Model/ISummitBadgeFeatureTypeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
* Interface ISummitBadgeFeatureTypeService
* @package App\Services\Model
*/
interface ISummitBadgeFeatureTypeService
interface ISummitBadgeFeatureTypeService extends IFilePostProcessorForChildEntity
{

/**
Expand Down Expand Up @@ -65,7 +65,7 @@ public function addFeatureImage
Summit $summit,
int $feature_id,
UploadedFile $file,
int $max_file_size = 10485760
int $max_file_size = SummitBadgeFeatureType::ImageMaxFileSize
):File;

/**
Expand Down
55 changes: 3 additions & 52 deletions app/Services/Model/Imp/CompanyService.php
Original file line number Diff line number Diff line change
Expand Up @@ -240,70 +240,21 @@ public function processFileForChildEntity(FileInfoDTO $file_info_dto): IEntity {
Log::debug(sprintf("CompanyService::processFileForChildEntity file_info_dto %s", $file_info_dto));
switch ($file_info_dto->owner_member_name) {
case 'big_logo':
return $this->processLogoFile($file_info_dto, [$this, 'addCompanyBigLogo']);
return self::processFileFromRemoteStorage($file_info_dto, [$this, 'addCompanyBigLogo']);
case 'logo':
return $this->processLogoFile($file_info_dto, [$this, 'addCompanyLogo']);
return self::processFileFromRemoteStorage($file_info_dto, [$this, 'addCompanyLogo']);
default:
Log::warning(sprintf("CompanyService::processFileForChildEntity unknown member name '%s'", $file_info_dto->owner_member_name));
throw new \InvalidArgumentException(sprintf("Unknown owner_member_name '%s' for entity class '%s'.", $file_info_dto->owner_member_name, $file_info_dto->owner_entity_class));
}
}

/**
* Downloads a file from remote storage to a local temp path, verifies its MD5 (when provided),
* invokes $uploader to persist it, then cleans up. On failure the remote file is preserved
* so queue retries can re-download it. Cleanup errors after a successful upload are logged
* but not re-thrown - upload success determines job success, not storage housekeeping.
*/
private function processLogoFile(FileInfoDTO $file_info_dto, callable $uploader): IEntity
{
$localPath = self::getFileFromRemoteStorageOnTempStorage(
$file_info_dto->filename,
$file_info_dto->filepath
);
$succeeded = false;
try {
if (!is_null($file_info_dto->md5)) {
$localHash = md5_file($localPath);
if ($localHash === false)
throw new ValidationException("File integrity check failed: unable to read local temp file.");
if ($localHash !== strtolower($file_info_dto->md5))
throw new ValidationException("File integrity check failed: MD5 mismatch.");
}
$file = new UploadedFile(
path: $localPath,
originalName: $file_info_dto->filename,
mimeType: $file_info_dto->mime_type,
error: null,
test: true,
);
$logo = $uploader($file_info_dto->owner_entity_id, $file);
$succeeded = true;
} finally {
if ($succeeded) {
try {
self::cleanLocalAndRemoteFile($localPath, $file_info_dto->filepath);
} catch (\Throwable $e) {
// Upload succeeded; cleanup failure is non-fatal. Log and continue so the
// job does not retry and create duplicate File records.
Log::warning(sprintf(
"CompanyService::processLogoFile cleanup failed after successful upload (filepath=%s): %s",
$file_info_dto->filepath,
$e->getMessage()
));
}
} else {
self::cleanLocalFile($localPath);
}
}
return $logo;
}

private function dispatchLogoJob(Company $company, string $memberName, array $payload): void
{
$file_upload_info = FileUploadInfo::buildFromPayload($payload);
if (is_null($file_upload_info)) return;

// TODO: also validate the size here (as SummitBadgeFeatureTypeService does) so an oversized logo returns 412 instead of failing the job silently
if (!in_array($file_upload_info->getFileExt(), Company::LogoAllowedExtensions))
throw new ValidationException(sprintf(
"%s file does not have a valid extension (%s).",
Expand Down
Loading
Loading