diff options
author | devianl2 <devianleong@gmail.com> | 2022-02-11 12:05:11 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-02-11 12:05:11 +0800 |
commit | 359e2588a4962f51c0d5cbdd2823530a15defee0 (patch) | |
tree | b9bc389a9734ccc9d496353205a7d859bfa7bb48 | |
parent | 9a3d5c62ac377bfcb9e31f98275636548c4d65ea (diff) |
Revert "Improve SCORM disk storage handler"
-rw-r--r-- | README.md | 34 | ||||
-rw-r--r-- | config/scorm.php | 18 | ||||
-rw-r--r-- | database/migrations/create_scorm_tables.php.stub | 14 | ||||
-rw-r--r-- | src/Entity/Scorm.php | 35 | ||||
-rw-r--r-- | src/Library/ScormLib.php | 30 | ||||
-rw-r--r-- | src/Manager/ScormDisk.php | 74 | ||||
-rw-r--r-- | src/Manager/ScormManager.php | 246 | ||||
-rw-r--r-- | src/Model/ScormScoModel.php | 11 |
8 files changed, 166 insertions, 296 deletions
@@ -32,42 +32,8 @@ php artisan vendor:publish --provider="Peopleaps\Scorm\ScormServiceProvider" ``` ## Step 3: -Run config cache for update cached configuration -```sh -php artisan config:cache -``` - -## Step 4: Migrate file to database ```sh php artisan migrate ``` -## Step 5 (Optional): -update SCORM config under config/scorm -- update scorm table names. -- update SCORM disk and configure disk @see config/filesystems.php -``` - 'disk' => 'scorm-local', - 'disk' => 'scorm-s3', - - // @see config/filesystems.php - 'disks' => [ - ..... - 'scorm-local' => [ - 'driver' => 'local', - 'root' => env('SCORM_ROOT_DIR'), // set root dir - 'visibility' => 'public', - ], - - 's3-scorm' => [ - 'driver' => 's3', - 'root' => env('SCORM_ROOT_DIR'), // set root dir - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => env('AWS_DEFAULT_REGION'), - 'bucket' => env('AWS_SCORM_BUCKET'), - ], - ..... - ] -``` diff --git a/config/scorm.php b/config/scorm.php index 0330bc5..e5efb3f 100644 --- a/config/scorm.php +++ b/config/scorm.php @@ -3,25 +3,11 @@ return [ 'table_names' => [ - 'user_table' => 'users', // user table name on main LMS app. + 'user_table' => 'users', 'scorm_table' => 'scorm', 'scorm_sco_table' => 'scorm_sco', 'scorm_sco_tracking_table' => 'scorm_sco_tracking', ], - /** - * Scorm directory. You may create a custom path in file system - * Define Scorm disk under @see config/filesystems.php - * 'disk' => 'local', - * 'disk' => 's3-scorm', - * ex. - * 's3-scorm' => [ - * 'driver' => 's3', - * 'root' => env('SCORM_ROOT_DIR'), // define root dir - * 'key' => env('AWS_ACCESS_KEY_ID'), - * 'secret' => env('AWS_SECRET_ACCESS_KEY'), - * 'region' => env('AWS_DEFAULT_REGION'), - * 'bucket' => env('AWS_SCORM_BUCKET'), - * ], - */ + // Scorm directory. You may create a custom path in file system 'disk' => 'local', ]; diff --git a/database/migrations/create_scorm_tables.php.stub b/database/migrations/create_scorm_tables.php.stub index 6a136b3..506e9cb 100644 --- a/database/migrations/create_scorm_tables.php.stub +++ b/database/migrations/create_scorm_tables.php.stub @@ -97,7 +97,19 @@ class CreateScormTables extends Migration */ public function down() { - $tableNames = config('scorm.table_names'); + $tableNames = config('scorm_sco_tracking_table'); + + if (empty($tableNames)) { + throw new \Exception('Error: Table not found.'); + } + + $tableNames = config('scorm_sco_table'); + + if (empty($tableNames)) { + throw new \Exception('Error: Table not found.'); + } + + $tableNames = config('scorm_table'); if (empty($tableNames)) { throw new \Exception('Error: Table not found.'); diff --git a/src/Entity/Scorm.php b/src/Entity/Scorm.php index 48fd711..4095986 100644 --- a/src/Entity/Scorm.php +++ b/src/Entity/Scorm.php @@ -3,6 +3,9 @@ namespace Peopleaps\Scorm\Entity; + +use Doctrine\Common\Collections\ArrayCollection; + class Scorm { const SCORM_12 = 'scorm_12'; @@ -10,9 +13,8 @@ class Scorm public $uuid; public $id; - public $title; public $version; - public $entryUrl; + public $hashName; public $ratio = 56.25; public $scos; public $scoSerializer; @@ -68,33 +70,17 @@ class Scorm /** * @return string */ - public function getTitle() - { - return $this->title; - } - - /** - * @param string $title - */ - public function setTitle($title) - { - $this->title = $title; - } - - /** - * @return string - */ - public function getEntryUrl() + public function getHashName() { - return $this->entryUrl; + return $this->hashName; } /** - * @param string $title + * @param string $hashName */ - public function setEntryUrl($entryUrl) + public function setHashName($hashName) { - $this->entryUrl = $entryUrl; + $this->hashName = $hashName; } /** @@ -145,8 +131,7 @@ class Scorm return [ 'id' => $scorm->getUuid(), 'version' => $scorm->getVersion(), - 'title' => $scorm->getTitle(), - 'entryUrl' => $scorm->getEntryUrl(), + 'hashName' => $scorm->getHashName(), 'ratio' => $scorm->getRatio(), 'scos' => $this->serializeScos($scorm), ]; diff --git a/src/Library/ScormLib.php b/src/Library/ScormLib.php index 731601a..0ca8a6b 100644 --- a/src/Library/ScormLib.php +++ b/src/Library/ScormLib.php @@ -7,7 +7,7 @@ namespace Peopleaps\Scorm\Library; use DOMDocument; use Peopleaps\Scorm\Entity\Sco; use Peopleaps\Scorm\Exception\InvalidScormArchiveException; -use Illuminate\Support\Str; +use Ramsey\Uuid\Uuid; class ScormLib { @@ -28,20 +28,16 @@ class ScormLib $organizations = $organizationsList->item(0); $organization = $organizations->firstChild; - if ( - !is_null($organizations->attributes) - && !is_null($organizations->attributes->getNamedItem('default')) - ) { + if (!is_null($organizations->attributes) + && !is_null($organizations->attributes->getNamedItem('default'))) { $defaultOrganization = $organizations->attributes->getNamedItem('default')->nodeValue; } else { $defaultOrganization = null; } // No default organization is defined if (is_null($defaultOrganization)) { - while ( - !is_null($organization) - && 'organization' !== $organization->nodeName - ) { + while (!is_null($organization) + && 'organization' !== $organization->nodeName) { $organization = $organization->nextSibling; } @@ -52,12 +48,10 @@ class ScormLib // A default organization is defined // Look for it else { - while ( - !is_null($organization) + while (!is_null($organization) && ('organization' !== $organization->nodeName || is_null($organization->attributes->getNamedItem('identifier')) - || $organization->attributes->getNamedItem('identifier')->nodeValue !== $defaultOrganization) - ) { + || $organization->attributes->getNamedItem('identifier')->nodeValue !== $defaultOrganization)) { $organization = $organization->nextSibling; } @@ -88,7 +82,7 @@ class ScormLib if ('item' === $item->nodeName) { $sco = new Sco(); $scos[] = $sco; - $sco->setUuid(Str::uuid()); + $sco->setUuid(Uuid::uuid4()); $sco->setScoParent($parentSco); $this->findAttrParams($sco, $item, $resources); $this->findNodeParams($sco, $item->firstChild); @@ -125,7 +119,7 @@ class ScormLib throw new InvalidScormArchiveException('sco_resource_without_href_message'); } $sco = new Sco(); - $sco->setUuid(Str::uuid()); + $sco->setUuid(Uuid::uuid4()); $sco->setBlock(false); $sco->setVisible(true); $sco->setIdentifier($identifier->nodeValue); @@ -201,12 +195,10 @@ class ScormLib case 'adlcp:timeLimitAction': $action = strtolower($item->nodeValue); - if ( - 'exit,message' === $action + if ('exit,message' === $action || 'exit,no message' === $action || 'continue,message' === $action - || 'continue,no message' === $action - ) { + || 'continue,no message' === $action) { $sco->setTimeLimitAction($action); } break; diff --git a/src/Manager/ScormDisk.php b/src/Manager/ScormDisk.php deleted file mode 100644 index 249f027..0000000 --- a/src/Manager/ScormDisk.php +++ /dev/null @@ -1,74 +0,0 @@ -<?php - -namespace Peopleaps\Scorm\Manager; - -use Illuminate\Filesystem\FilesystemAdapter; -use Illuminate\Support\Facades\Storage; -use Peopleaps\Scorm\Exception\StorageNotFoundException; -use ZipArchive; - -class ScormDisk -{ - /** - * Extract zip file into destination directory. - * - * @param string $path Destination directory - * @param string $zipFilePath The path to the zip file. - * - * @return bool True on success, false on failure. - */ - public function unzip($file, $path) - { - $path = $this->cleanPath($path); - - $zipArchive = new ZipArchive(); - if ($zipArchive->open($file) !== true) { - return false; - } - - /** @var FilesystemAdapter $disk */ - $disk = $this->getDisk(); - - for ($i = 0; $i < $zipArchive->numFiles; ++$i) { - $zipEntryName = $zipArchive->getNameIndex($i); - $destination = $path . DIRECTORY_SEPARATOR . $this->cleanPath($zipEntryName); - if ($this->isDirectory($zipEntryName)) { - $disk->createDir($destination); - continue; - } - $disk->putStream($destination, $zipArchive->getStream($zipEntryName)); - } - - return true; - } - - /** - * @param string $directory - * @return bool - */ - public function deleteScormFolder($folderHashedName) - { - return $this->getDisk()->deleteDirectory($folderHashedName); - } - - private function isDirectory($zipEntryName) - { - return substr($zipEntryName, -1) === '/'; - } - - private function cleanPath($path) - { - return str_replace('/', DIRECTORY_SEPARATOR, $path); - } - - /** - * @return FilesystemAdapter $disk - */ - private function getDisk() - { - if (!config()->has('filesystems.disks.' . config('scorm.disk'))) { - throw new StorageNotFoundException('scorm_disk_not_define'); - } - return Storage::disk(config('scorm.disk')); - } -} diff --git a/src/Manager/ScormManager.php b/src/Manager/ScormManager.php index 56c723c..2a54706 100644 --- a/src/Manager/ScormManager.php +++ b/src/Manager/ScormManager.php @@ -3,28 +3,31 @@ namespace Peopleaps\Scorm\Manager; +use App\Models\User; use Carbon\Carbon; use DOMDocument; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Storage; +use League\Flysystem\FileNotFoundException; +use Peopleaps\Scorm\Entity\Sco; use Peopleaps\Scorm\Entity\Scorm; use Peopleaps\Scorm\Entity\ScoTracking; use Peopleaps\Scorm\Exception\InvalidScormArchiveException; +use Peopleaps\Scorm\Exception\StorageNotFoundException; use Peopleaps\Scorm\Library\ScormLib; use Peopleaps\Scorm\Model\ScormModel; use Peopleaps\Scorm\Model\ScormScoModel; use Peopleaps\Scorm\Model\ScormScoTrackingModel; -use Illuminate\Support\Str; -use Peopleaps\Scorm\Entity\Sco; +use Ramsey\Uuid\Uuid; use ZipArchive; class ScormManager { /** @var ScormLib */ private $scormLib; - /** @var ScormDisk */ - private $scormDisk; /** * Constructor. @@ -32,18 +35,18 @@ class ScormManager * @param string $filesDir * @param string $uploadDir */ - public function __construct() - { + public function __construct( + ) { $this->scormLib = new ScormLib(); - $this->scormDisk = new ScormDisk(); } - public function uploadScormArchive(UploadedFile $file) + public function uploadScormArchive(UploadedFile $file, Model $model) { // Checks if it is a valid scorm archive $scormData = null; $zip = new ZipArchive(); $openValue = $zip->open($file); + $oldModel = null; $isScormArchive = (true === $openValue) && $zip->getStream('imsmanifest.xml'); @@ -55,71 +58,57 @@ class ScormManager $scormData = $this->generateScorm($file); } + $oldModel = $model->scorm()->first(); // get old scorm data for deletion (If success to store new) + // save to db - if (is_null($scormData) || !is_array($scormData)) { - throw new InvalidScormArchiveException('invalid_scorm_data'); - } + if ($scormData && is_array($scormData)) { - $scorm = ScormModel::whereOriginFile($scormData['identifier']); - // Check if scom package already exists to drop old one. - if (!$scorm->exists()) { - $scorm = new ScormModel(); - } else { - $scorm = $scorm->first(); - $this->deleteScormData($scorm); - } + $scorm = new ScormModel(); + $scorm->version = $scormData['version']; + $scorm->hash_name = $scormData['hashName']; + $scorm->origin_file = $scormData['name']; + $scorm->origin_file_mime = $scormData['type']; + $scorm->uuid = $scormData['hashName']; + + $scorm = $model->scorm()->save($scorm); - $scorm->uuid = $scormData['uuid']; - $scorm->title = $scormData['title']; - $scorm->version = $scormData['version']; - $scorm->entry_url = $scormData['entryUrl']; - $scorm->origin_file = $scormData['identifier']; - $scorm->save(); - - if (!empty($scormData['scos']) && is_array($scormData['scos'])) { - /** @var Sco $scoData */ - foreach ($scormData['scos'] as $scoData) { - $sco = $this->saveScormScos($scorm->id, $scoData); - if ($scoData->scoChildren) { - foreach ($scoData->scoChildren as $scoChild) { - $this->saveScormScos($scorm->id, $scoChild, $sco->id); + if (!empty($scormData['scos']) && is_array($scormData['scos'])) { + foreach ($scormData['scos'] as $scoData) { + + $scoParent = null; + if (!empty($scoData->scoParent)) { + $scoParent = ScormScoModel::where('uuid', $scoData->scoParent->uuid)->first(); } + + $sco = new ScormScoModel(); + $sco->scorm_id = $scorm->id; + $sco->uuid = $scoData->uuid; + $sco->sco_parent_id = $scoParent ? $scoParent->id : null; + $sco->entry_url = $scoData->entryUrl; + $sco->identifier = $scoData->identifier; + $sco->title = $scoData->title; + $sco->visible = $scoData->visible; + $sco->sco_parameters = $scoData->parameters; + $sco->launch_data = $scoData->launchData; + $sco->max_time_allowed = $scoData->maxTimeAllowed; + $sco->time_limit_action = $scoData->timeLimitAction; + $sco->block = $scoData->block; + $sco->score_int = $scoData->scoreToPassInt; + $sco->score_decimal = $scoData->scoreToPassDecimal; + $sco->completion_threshold = $scoData->completionThreshold; + $sco->prerequisites = $scoData->prerequisites; + $sco->save(); } } + + if ($oldModel != null) { + $this->deleteScormData($oldModel); + } } return $scormData; } - /** - * Save Scorm sco and it's nested children - * @param int $scorm_id scorm id. - * @param Sco $scoData Sco data to be store. - * @param int $sco_parent_id sco parent id for children - */ - private function saveScormScos($scorm_id, $scoData, $sco_parent_id = null) - { - $sco = new ScormScoModel(); - $sco->scorm_id = $scorm_id; - $sco->uuid = $scoData->uuid; - $sco->sco_parent_id = $sco_parent_id; - $sco->entry_url = $scoData->entryUrl; - $sco->identifier = $scoData->identifier; - $sco->title = $scoData->title; - $sco->visible = $scoData->visible; - $sco->sco_parameters = $scoData->parameters; - $sco->launch_data = $scoData->launchData; - $sco->max_time_allowed = $scoData->maxTimeAllowed; - $sco->time_limit_action = $scoData->timeLimitAction; - $sco->block = $scoData->block; - $sco->score_int = $scoData->scoreToPassInt; - $sco->score_decimal = $scoData->scoreToPassDecimal; - $sco->completion_threshold = $scoData->completionThreshold; - $sco->prerequisites = $scoData->prerequisites; - $sco->save(); - return $sco; - } - private function parseScormArchive(UploadedFile $file) { $data = []; @@ -140,18 +129,8 @@ class ScormManager throw new InvalidScormArchiveException('cannot_load_imsmanifest_message'); } - $manifest = $dom->getElementsByTagName('manifest')->item(0); - if (!is_null($manifest->attributes->getNamedItem('identifier'))) { - $data['identifier'] = $manifest->attributes->getNamedItem('identifier')->nodeValue; - } else { - throw new InvalidScormArchiveException('invalid_scorm_manifest_identifier'); - } - $titles = $dom->getElementsByTagName('title'); - if ($titles->length > 0) { - $data['title'] = Str::of($titles->item(0)->textContent)->trim('/n')->trim(); - } - $scormVersionElements = $dom->getElementsByTagName('schemaversion'); + if ($scormVersionElements->length > 0) { switch ($scormVersionElements->item(0)->textContent) { case '1.2': @@ -173,43 +152,69 @@ class ScormManager if (0 >= count($scos)) { throw new InvalidScormArchiveException('no_sco_in_scorm_archive_message'); } - - $data['entryUrl'] = $scos[0]->entryUrl ?? $scos[0]->scoChildren[0]->entryUrl; $data['scos'] = $scos; return $data; } - public function deleteScorm($model) - { + public function deleteScormData($model) { // Delete after the previous item is stored if ($model) { - $this->deleteScormData($model); + + $oldScos = $model->scos()->get(); + + // Delete all tracking associate with sco + foreach ($oldScos as $oldSco) { + $oldSco->scoTrackings()->delete(); + } + + $model->scos()->delete(); // delete scos $model->delete(); // delete scorm + + // Delete folder from server + $this->deleteScormFolder($model->hash_name); } } + /** + * @param $folderHashedName + * @return bool + */ + protected function deleteScormFolder($folderHashedName) { - private function deleteScormData($model) - { - // Delete after the previous item is stored - $oldScos = $model->scos()->get(); + $response = Storage::disk('scorm')->deleteDirectory($folderHashedName); - // Delete all tracking associate with sco - foreach ($oldScos as $oldSco) { - $oldSco->scoTrackings()->delete(); - } - $model->scos()->delete(); // delete scos - // Delete folder from server - $this->deleteScormFolder($model->uuid); + return $response; } /** - * @param $folderHashedName - * @return bool + * Unzip a given ZIP file into the web resources directory. + * + * @param string $hashName name of the destination directory */ - protected function deleteScormFolder($folderHashedName) + private function unzipScormArchive(UploadedFile $file, $hashName) { - return $this->scormDisk->deleteScormFolder($folderHashedName); + $zip = new \ZipArchive(); + $zip->open($file); + + if (!config()->has('filesystems.disks.'.config('scorm.disk').'.root')) { + throw new StorageNotFoundException(); + } + + $rootFolder = config('filesystems.disks.'.config('scorm.disk').'.root'); + + if (substr($rootFolder, -1) != '/') { + // If end with xxx/ + $rootFolder = config('filesystems.disks.'.config('scorm.disk').'.root').'/'; + } + + $destinationDir = $rootFolder.$hashName; // file path + + if (!File::isDirectory($destinationDir)) { + File::makeDirectory($destinationDir, 0755, true, true); + } + + $zip->extractTo($destinationDir); + $zip->close(); } /** @@ -219,21 +224,32 @@ class ScormManager */ private function generateScorm(UploadedFile $file) { - $uuid = Str::uuid(); + $hashName = Uuid::uuid4(); + $hashFileName = $hashName.'.zip'; $scormData = $this->parseScormArchive($file); - /** - * Unzip a given ZIP file into the web resources directory. - * - * @param string $hashName name of the destination directory - */ - $this->scormDisk->unzip($file, $uuid); + $this->unzipScormArchive($file, $hashName); + + if (!config()->has('filesystems.disks.'.config('scorm.disk').'.root')) { + throw new StorageNotFoundException(); + } + + $rootFolder = config('filesystems.disks.'.config('scorm.disk').'.root'); + + if (substr($rootFolder, -1) != '/') { + // If end with xxx/ + $rootFolder = config('filesystems.disks.'.config('scorm.disk').'.root').'/'; + } + + $destinationDir = $rootFolder.$hashName; // file path + + // Move Scorm archive in the files directory + $finalFile = $file->move($destinationDir, $hashName.'.zip'); return [ - 'identifier' => $scormData['identifier'], - 'uuid' => $uuid, - 'title' => $scormData['title'], // to follow standard file data format + 'name' => $hashFileName, // to follow standard file data format + 'hashName' => $hashName, + 'type' => $finalFile->getMimeType(), 'version' => $scormData['version'], - 'entryUrl' => $scormData['entryUrl'], 'scos' => $scormData['scos'], ]; } @@ -243,8 +259,7 @@ class ScormManager * @param $scormId * @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection */ - public function getScos($scormId) - { + public function getScos($scormId) { $scos = ScormScoModel::with([ 'scorm' ])->where('scorm_id', $scormId) @@ -258,8 +273,7 @@ class ScormManager * @param $scoUuid * @return null|\Illuminate\Database\Eloquent\Builder|Model */ - public function getScoByUuid($scoUuid) - { + public function getScoByUuid($scoUuid) { $sco = ScormScoModel::with([ 'scorm' ])->where('uuid', $scoUuid) @@ -268,8 +282,7 @@ class ScormManager return $sco; } - public function getUserResult($scoId, $userId) - { + public function getUserResult($scoId, $userId) { return ScormScoTrackingModel::where('sco_id', $scoId)->where('user_id', $userId)->first(); } @@ -314,7 +327,7 @@ class ScormManager 'user_id' => $userId, 'sco_id' => $sco->id ], [ - 'uuid' => Str::uuid(), + 'uuid' => Uuid::uuid4(), 'progression' => $scoTracking->getProgression(), 'score_raw' => $scoTracking->getScoreRaw(), 'score_min' => $scoTracking->getScoreMin(), @@ -362,8 +375,7 @@ class ScormManager return $scoTracking; } - public function findScoTrackingId($scoUuid, $scoTrackingUuid) - { + public function findScoTrackingId($scoUuid, $scoTrackingUuid) { return ScormScoTrackingModel::with([ 'sco' ])->whereHas('sco', function (Builder $query) use ($scoUuid) { @@ -372,8 +384,7 @@ class ScormManager ->firstOrFail(); } - public function checkUserIsCompletedScorm($scormId, $userId) - { + public function checkUserIsCompletedScorm($scormId, $userId) { $completedSco = []; $scos = ScormScoModel::where('scorm_id', $scormId)->get(); @@ -527,8 +538,7 @@ class ScormManager $bestStatus = $lessonStatus; } - if ( - empty($tracking->getCompletionStatus()) + if (empty($tracking->getCompletionStatus()) || ($completionStatus !== $tracking->getCompletionStatus() && $statusPriority[$completionStatus] > $statusPriority[$tracking->getCompletionStatus()]) ) { // This is no longer needed as completionStatus and successStatus are merged together @@ -620,7 +630,7 @@ class ScormManager $remainingTime %= 3600; $nbMinutes = (int) ($remainingTime / 60); $nbSeconds = $remainingTime % 60; - $result .= 'P' . $nbDays . 'DT' . $nbHours . 'H' . $nbMinutes . 'M' . $nbSeconds . 'S'; + $result .= 'P'.$nbDays.'DT'.$nbHours.'H'.$nbMinutes.'M'.$nbSeconds.'S'; } return $result; diff --git a/src/Model/ScormScoModel.php b/src/Model/ScormScoModel.php index d606d32..de50741 100644 --- a/src/Model/ScormScoModel.php +++ b/src/Model/ScormScoModel.php @@ -13,18 +13,11 @@ class ScormScoModel extends Model return config('scorm.table_names.scorm_sco_table', parent::getTable()); } - public function scorm() - { + public function scorm() { return $this->belongsTo(ScormModel::class, 'scorm_id', 'id'); } - public function scoTrackings() - { + public function scoTrackings() { return $this->hasMany(ScormScoTrackingModel::class, 'sco_id', 'id'); } - - public function children() - { - return $this->hasMany(ScormScoModel::class, 'sco_parent_id', 'id'); - } } |