Skip to content
Merged
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
92 changes: 86 additions & 6 deletions app/Http/Controllers/ResourcesImportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Imports\ResourcesImport;
use App\Imports\ResourcesPreviewImport;
use App\Services\LearnTeachWorkbookParser;
use App\Services\ResourcesImportResult;
use App\Services\ResourcesUploadValidator;
use Illuminate\Http\RedirectResponse;
Expand All @@ -14,6 +15,7 @@
use Illuminate\Support\Str;
use Illuminate\View\View;
use Maatwebsite\Excel\Facades\Excel;
use ZipArchive;

class ResourcesImportController extends Controller
{
Expand All @@ -38,7 +40,7 @@ public function verify(Request $request): RedirectResponse
'file' => [
'required',
'file',
'max:10240',
'max:51200',
function ($attribute, $value, $fail) {
if ($value) {
$ext = strtolower($value->getClientOriginalExtension());
Expand All @@ -52,10 +54,12 @@ function ($attribute, $value, $fail) {
}
},
],
'assets_zip' => ['nullable', 'file', 'mimes:zip', 'max:512000'],
'focus' => ['nullable', 'boolean'],
], [
'file.required' => 'Please select a file to upload.',
'file.max' => 'The file may not be greater than 10 MB.',
'file.max' => 'The file may not be greater than 50 MB.',
'assets_zip.max' => 'The assets ZIP may not be greater than 500 MB.',
]);

$file = $request->file('file');
Expand Down Expand Up @@ -84,13 +88,28 @@ function ($attribute, $value, $fail) {
$request->session()->forget([self::SESSION_FILE_PATH, self::SESSION_ROWS, self::SESSION_FOCUS]);

$path = $file->storeAs('temp', 'resources_import_'.time().'.'.$extension, $tempDisk);
$absolutePath = Storage::disk($tempDisk)->path($path);
$assetsDir = null;

if ($request->hasFile('assets_zip')) {
try {
$assetsDir = $this->extractAssetsZip($request->file('assets_zip'), $tempDisk);
} catch (\Throwable $e) {
Storage::disk($tempDisk)->delete($path);

return redirect()->route('admin.resources-import.index')
->withErrors(['assets_zip' => 'Could not extract assets ZIP: '.$e->getMessage()])
->withInput();
}
}

try {
$import = new ResourcesPreviewImport;
Excel::import($import, $path, $tempDisk);
$rows = $import->data;
$rows = $this->parseUploadedMetadata($absolutePath, $path, $extension, $tempDisk);
} catch (\Throwable $e) {
Storage::disk($tempDisk)->delete($path);
if ($assetsDir) {
Storage::disk($tempDisk)->deleteDirectory($assetsDir);
}

return redirect()->route('admin.resources-import.index')
->withErrors(['file' => 'Could not parse file: '.$e->getMessage()])
Expand All @@ -100,13 +119,19 @@ function ($attribute, $value, $fail) {
$headerCheck = ResourcesUploadValidator::validateRequiredColumnsFromRows($rows);
if (! $headerCheck['valid']) {
Storage::disk($tempDisk)->delete($path);
if ($assetsDir) {
Storage::disk($tempDisk)->deleteDirectory($assetsDir);
}

return redirect()->route('admin.resources-import.index')
->withErrors(['file' => 'Missing required column(s): '.implode(', ', $headerCheck['missing']).'. Please add a header row with at least "name_of_the_resource".'])
->withInput();
}
if (empty($rows)) {
Storage::disk($tempDisk)->delete($path);
if ($assetsDir) {
Storage::disk($tempDisk)->deleteDirectory($assetsDir);
}

return redirect()->route('admin.resources-import.index')
->withErrors(['file' => 'The file has no data rows.'])
Expand All @@ -123,6 +148,7 @@ function ($attribute, $value, $fail) {
'path' => $path,
'disk' => $tempDisk,
'focus' => $focus,
'assets_dir' => $assetsDir,
], now()->addHours(1));
$request->session()->put('resources_import_token', $importToken);

Expand Down Expand Up @@ -172,6 +198,7 @@ public function import(Request $request): RedirectResponse
{
$path = null;
$focus = false;
$assetsDir = null;
$tempDisk = config('filesystems.resources_import_temp_disk', 'local');

$token = $request->input('import_token');
Expand All @@ -181,6 +208,7 @@ public function import(Request $request): RedirectResponse
$path = $cached['path'];
$tempDisk = $cached['disk'] ?? $tempDisk;
$focus = (bool) ($cached['focus'] ?? false);
$assetsDir = $cached['assets_dir'] ?? null;
}
}

Expand Down Expand Up @@ -266,10 +294,15 @@ public function import(Request $request): RedirectResponse

try {
$result = new ResourcesImportResult;
$import = new ResourcesImport(null, null, $focus, $overrides, $result, $filenameMode, $batchTimestamp, $customSuffix);
$imagesDir = $assetsDir ? Storage::disk($tempDisk)->path($assetsDir.'/images') : null;
$pdfsDir = $assetsDir ? Storage::disk($tempDisk)->path($assetsDir.'/links') : null;
$import = new ResourcesImport($imagesDir, $pdfsDir, $focus, $overrides, $result, $filenameMode, $batchTimestamp, $customSuffix);
Excel::import($import, $path, $tempDisk);

Storage::disk($tempDisk)->delete($path);
if ($assetsDir) {
Storage::disk($tempDisk)->deleteDirectory($assetsDir);
}
$request->session()->forget([self::SESSION_FILE_PATH, self::SESSION_ROWS, self::SESSION_FOCUS, 'resources_import_token']);
if (is_string($token = $request->input('import_token')) && $token !== '') {
Cache::forget('resources_import_' . $token);
Expand All @@ -284,6 +317,9 @@ public function import(Request $request): RedirectResponse
if (Storage::disk($tempDisk)->exists($path)) {
Storage::disk($tempDisk)->delete($path);
}
if ($assetsDir) {
Storage::disk($tempDisk)->deleteDirectory($assetsDir);
}
$request->session()->forget([self::SESSION_FILE_PATH, self::SESSION_ROWS, self::SESSION_FOCUS, 'resources_import_token']);
if (is_string($token = $request->input('import_token')) && $token !== '') {
Cache::forget('resources_import_' . $token);
Expand Down Expand Up @@ -314,4 +350,48 @@ public function report(Request $request): View|RedirectResponse
'failures' => $failures ?? [],
]);
}

/**
* @return array<int, array<string, mixed>>
*/
private function parseUploadedMetadata(string $absolutePath, string &$storedPath, string $extension, string $disk): array
{
if (in_array($extension, ['xlsx', 'xls'], true) && LearnTeachWorkbookParser::looksLikeLearnTeachWorkbook($absolutePath)) {
$parser = new LearnTeachWorkbookParser;
$rows = $parser->parse($absolutePath);
$csvPath = 'temp/resources_import_'.time().'.csv';
$parser->writeCsv($rows, Storage::disk($disk)->path($csvPath));
Storage::disk($disk)->delete($storedPath);
$storedPath = $csvPath;

return $rows;
}

$import = new ResourcesPreviewImport;
Excel::import($import, $storedPath, $disk);

return $import->data;
}

private function extractAssetsZip(\Illuminate\Http\UploadedFile $zipFile, string $disk): string
{
$assetsDir = 'temp/learn_teach_assets_'.time();
Storage::disk($disk)->makeDirectory($assetsDir);
$targetPath = Storage::disk($disk)->path($assetsDir);

$zip = new ZipArchive;
$opened = $zip->open($zipFile->getRealPath());
if ($opened !== true) {
throw new \RuntimeException('Invalid ZIP archive.');
}

if (! $zip->extractTo($targetPath)) {
$zip->close();
throw new \RuntimeException('ZIP extraction failed.');
}

$zip->close();

return $assetsDir;
}
}
68 changes: 66 additions & 2 deletions app/Imports/ResourcesImport.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use App\ResourceSubject;
use App\ResourceCategory;
use App\ResourceLanguage;
use App\Services\SharePointAssetFetcher;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
Expand Down Expand Up @@ -206,7 +207,7 @@ protected function processRow(array $row, int $rowIndex): ?Model
$imageValue = trim((string) ($row['image'] ?? ''));
if ($imageValue !== '') {
if (str_starts_with($imageValue, 'http://') || str_starts_with($imageValue, 'https://')) {
$thumbnail = $imageValue;
$thumbnail = $this->uploadRemoteImage($imageValue, $row, $rowIndex) ?? $imageValue;
} elseif ($this->imagesDir) {
$localPath = $this->imagesDir . DIRECTORY_SEPARATOR . $imageValue;
if (file_exists($localPath)) {
Expand All @@ -228,7 +229,10 @@ protected function processRow(array $row, int $rowIndex): ?Model
}

$pdfLink = null;
if (!empty($row['link']) && stripos($row['link'], 'http://') !== 0 && stripos($row['link'], 'https://') !== 0 && $this->pdfsDir) {
$linkValue = trim((string) ($row['link'] ?? ''));
if ($linkValue !== '' && $this->isHttpUrl($linkValue)) {
$pdfLink = $this->uploadRemotePdf($linkValue, $row, $rowIndex);
} elseif ($linkValue !== '' && $this->pdfsDir) {
$groupName = !empty($row['group_name']) ? trim($row['group_name']) : '';
$groupSlug = $groupName ? Str::slug($groupName) : 'default';

Expand Down Expand Up @@ -405,4 +409,64 @@ protected function processRow(array $row, int $rowIndex): ?Model

return $item;
}

protected function isHttpUrl(string $value): bool
{
return str_starts_with($value, 'http://') || str_starts_with($value, 'https://');
}

protected function uploadRemotePdf(string $url, array $row, int $rowIndex): ?string
{
/** @var SharePointAssetFetcher $fetcher */
$fetcher = app(SharePointAssetFetcher::class);
if (! $fetcher->isSharePointUrl($url) || ! $fetcher->looksLikePdfUrl($url)) {
return null;
}

$bytes = $fetcher->fetch($url);
if ($bytes === null || ! str_starts_with($bytes, '%PDF')) {
return null;
}

$groupName = ! empty($row['group_name']) ? trim((string) $row['group_name']) : '';
$groupSlug = $groupName ? Str::slug($groupName) : 'default';
$baseSlug = Str::slug(pathinfo(parse_url($url, PHP_URL_PATH) ?? 'resource', PATHINFO_FILENAME) ?: 'resource');
$basename = $this->filenameMode === 'preserve'
? $this->preserveModeBasename(basename(parse_url($url, PHP_URL_PATH) ?: 'resource.pdf'))
: $this->buildStoredBasename($baseSlug, 'pdf', $row, $rowIndex, false);
$storagePath = $groupSlug.'/'.$basename;
Storage::disk($this->disk)->put($storagePath, $bytes);

return Storage::disk($this->disk)->url($storagePath);
}

protected function uploadRemoteImage(string $url, array $row, int $rowIndex): ?string
{
/** @var SharePointAssetFetcher $fetcher */
$fetcher = app(SharePointAssetFetcher::class);
if (! $this->isHttpUrl($url)) {
return null;
}

$bytes = $fetcher->fetch($url);
if ($bytes === null || str_starts_with($bytes, '<')) {
return null;
}

$path = parse_url($url, PHP_URL_PATH);
$filename = is_string($path) && $path !== '' ? basename($path) : 'thumbnail.jpg';
$ext = pathinfo($filename, PATHINFO_EXTENSION) ?: 'jpg';
$basename = $this->filenameMode === 'preserve'
? $this->preserveModeBasename($filename)
: $this->buildStoredBasename(
Str::slug($row['name_of_the_resource'] ?? 'resource'),
$ext,
$row,
$rowIndex,
true
);
Storage::disk($this->disk)->put($basename, $bytes);

return Storage::disk($this->disk)->url($basename);
}
}
Loading
Loading