diff --git a/CHANGELOG.md b/CHANGELOG.md index e67b892..a2cfec0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Compress Changelog +## 4.0.1 - 2022-06-17 +### Added +- Added "Default Volume Subdirectory" settings to control where assets are stored. +- Added ability to specify output archive name +- Added setting to delete stale archives during garbage collection. +### Changed +- Archives created in volumes without public URLs will now be proxied through the server to be fulfilled +- Files are now streamed into archives instead of being copied to temporary files +- Running "getLazyLink" will now always return a controller URL instead of a direct link to assets. This should help prevent caching issues. + ## 4.0.0 - 2022-06-16 ### Change - Compress now requires Craft 4 diff --git a/README.md b/README.md index 5da31d7..b4217b4 100644 --- a/README.md +++ b/README.md @@ -54,12 +54,12 @@ settings and life choices. Second parameter is whether we want the archive generated on page load or lazily. #} - {% set archive = craft.compress.zip(assets, true) %} + {% set archive = craft.compress.zip(assets, true, 'My Photos') %} {# the archive variable is now set to an Archive model, but since we're in lazy mode, the getAsset() response may be null. We can - either check the .isReady method or we can just get the lazyLink + either check the .isReady method or we can just get the lazyLink, which will give us an indirect link to the asset. #} {% if archive.isReady %} {% set archiveAsset = archive.getAsset() %} @@ -98,11 +98,8 @@ generates a lazy link to download all assets of a particular kind. ## Caveats & Limitations - Consider the Assets created by Compress to be temporary. Don't try to use them in Asset relation fields. -- There's currently no way to override the filename on archives due to -technical limitations. -- There's currently nothing to purge stale archive assets, so if you have a -template that queries a variable set of assets, each time the result set changes, a new archive asset will be created and the prior will not be automatically deleted. +- When you provide a name for your archive, it's a good idea to ensure that name is unique to the files you're zipping up. Failure to do so could result in the file not being cached well and being constantly overwritten. Brought to you by [Venveo](https://www.venveo.com) diff --git a/composer.json b/composer.json index 03404cd..4f4cb18 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "venveo/craft-compress", "description": "Create smart zip files from Craft assets on the fly", "type": "craft-plugin", - "version": "4.0.0", + "version": "4.0.1", "keywords": [ "craft", "cms", diff --git a/src/Compress.php b/src/Compress.php index 0c94038..9f6b0c8 100644 --- a/src/Compress.php +++ b/src/Compress.php @@ -15,6 +15,7 @@ use craft\elements\Asset; use craft\events\ModelEvent; use craft\events\RegisterComponentTypesEvent; +use craft\services\Gc; use craft\services\Utilities; use craft\web\twig\variables\CraftVariable; use venveo\compress\models\Settings; @@ -34,27 +35,18 @@ */ class Compress extends Plugin { - // Static Properties - // ========================================================================= - /** * @var Compress */ public static $plugin; - // Public Properties - // ========================================================================= - /** * @var string */ - public string $schemaVersion = '1.0.0'; + public string $schemaVersion = '4.0.1'; public bool $hasCpSettings = true; - // Public Methods - // ========================================================================= - /** * @inheritdoc */ @@ -63,10 +55,6 @@ public function init() parent::init(); self::$plugin = $this; - $this->setComponents([ - 'compress' => CompressService::class - ]); - Event::on( CraftVariable::class, CraftVariable::EVENT_INIT, @@ -99,6 +87,12 @@ function (ModelEvent $event) { } ); + Event::on(Gc::class, Gc::EVENT_RUN, function () { + if ($this->getSettings()->deleteStaleArchivesHours) { + $this->compress->deleteStaleArchives(25); + } + }); + // Register our utility Event::on( @@ -111,13 +105,25 @@ function (RegisterComponentTypesEvent $event) { } + /** + * @inheritdoc + */ + public static function config(): array + { + return [ + 'components' => [ + 'compress' => ['class' => CompressService::class] + ], + ]; + } + // Protected Methods // ========================================================================= /** * @inheritdoc */ - protected function createSettingsModel(): ?\craft\base\Model + protected function createSettingsModel(): Settings { return new Settings(); } diff --git a/src/config.php b/src/config.php index 177915e..25b8e67 100644 --- a/src/config.php +++ b/src/config.php @@ -29,6 +29,16 @@ * Default: null */ 'defaultVolumeHandle' => null, + + /** + * How many hours do we wait before an archive is considered stale? + */ + 'deleteStaleArchivesHours' => 0, + + /** + * An optional subdirectory to put zipped files in + */ + 'defaultVolumeSubdirectory' => '', /** * If set to true, queue jobs will be dispatched to regenerate an archive diff --git a/src/controllers/CompressController.php b/src/controllers/CompressController.php index 018a91f..0676d2e 100644 --- a/src/controllers/CompressController.php +++ b/src/controllers/CompressController.php @@ -10,6 +10,7 @@ namespace venveo\compress\controllers; +use craft\helpers\App; use craft\helpers\DateTimeHelper; use craft\web\Controller; use venveo\compress\Compress as Plugin; @@ -17,6 +18,7 @@ use venveo\compress\models\Archive as ArchiveModel; use venveo\compress\records\Archive as ArchiveRecord; use yii\base\InvalidConfigException; +use yii\web\RangeNotSatisfiableHttpException; /** * Class CompressController @@ -32,6 +34,7 @@ class CompressController extends Controller * @return \yii\web\Response * @throws CompressException * @throws InvalidConfigException + * @throws RangeNotSatisfiableHttpException */ public function actionGetLink($uid) { @@ -41,19 +44,15 @@ public function actionGetLink($uid) return \Craft::$app->response->setStatusCode(404, 'Archive could not be found'); } - // If the asset is ready, redirect to its URL + // If the asset is ready, send it over if ($record->assetId) { $archiveModel = ArchiveModel::hydrateFromRecord($record); // It's possible for an asset ID to exist, but getAsset to return false on soft-deleted assets - if ($archiveModel->getAsset()) { - $assetUrl = $archiveModel->getAsset()->getUrl(); - if ($assetUrl) { - $record->dateLastAccessed = DateTimeHelper::currentUTCDateTime(); - $record->save(); - return \Craft::$app->response->redirect($archiveModel->getAsset()->getUrl()); - } - - return \Craft::$app->response->setStatusCode(404, 'Could not produce zip file URL.'); + $archiveAsset = $archiveModel->getAsset(); + if ($archiveAsset) { + $record->dateLastAccessed = DateTimeHelper::currentUTCDateTime(); + $record->save(); + return $this->getAssetResponse($archiveModel->getAsset()); } } @@ -71,11 +70,35 @@ public function actionGetLink($uid) \Craft::$app->cache->delete($cacheKey . ':jobId'); } } - return \Craft::$app->response->redirect($archiveModel->asset->getUrl()); + return $this->getAssetResponse($archiveModel->getAsset()); } catch (\Exception $e) { \Craft::error('Archive could not be generated: ' . $e->getMessage(), __METHOD__); \Craft::error($e->getTraceAsString(), __METHOD__); - throw new CompressException('Archive could not be generated: ' . $e->getMessage()); + return \Craft::$app->response->setStatusCode(404, 'Could not produce zip file URL.'); + } + } + + /** + * @param $archiveAsset + * @return \craft\web\Response|\yii\console\Response|\yii\web\Response + * @throws RangeNotSatisfiableHttpException + */ + protected function getAssetResponse($archiveAsset) { + if (!$archiveAsset) { + return \Craft::$app->response->setStatusCode(404, 'Could not produce zip file URL.'); + } + $assetUrl = $archiveAsset->getUrl(); + + // If we have a public URL for the asset, we'll just 302 redirect to it + if ($assetUrl) { + return \Craft::$app->response->redirect($archiveAsset->getUrl()); } + App::maxPowerCaptain(); + // No public URLs, we'll need to stream the response. + return $this->response + ->sendStreamAsFile($archiveAsset->getStream(), $archiveAsset->getFilename(), [ + 'fileSize' => $archiveAsset->size, + 'mimeType' => $archiveAsset->getMimeType(), + ]); } } diff --git a/src/migrations/Install.php b/src/migrations/Install.php index 3a2b93c..283c61d 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -58,6 +58,7 @@ protected function createTables() 'dateUpdated' => $this->dateTime()->notNull(), 'dateLastAccessed' => $this->dateTime()->notNull(), 'uid' => $this->uid(), + 'filename' => $this->string(), 'assetId' => $this->integer(), 'hash' => $this->string()->notNull(), ] diff --git a/src/migrations/m220617_145938_add_filename_support.php b/src/migrations/m220617_145938_add_filename_support.php new file mode 100644 index 0000000..02f5b1d --- /dev/null +++ b/src/migrations/m220617_145938_add_filename_support.php @@ -0,0 +1,30 @@ +addColumn('{{%compress_archives}}', 'filename', $this->string()->after('uid')); + return true; + } + + /** + * @inheritdoc + */ + public function safeDown(): bool + { + echo "m220617_145938_add_filename_support cannot be reverted.\n"; + return false; + } +} diff --git a/src/models/Archive.php b/src/models/Archive.php index ce1c2e0..ed9bdbc 100644 --- a/src/models/Archive.php +++ b/src/models/Archive.php @@ -13,9 +13,11 @@ use craft\base\Model; use craft\db\ActiveRecord; use craft\elements\Asset; +use craft\elements\db\AssetQuery; use craft\helpers\UrlHelper; use DateTime; use venveo\compress\Compress as Plugin; +use yii\base\InvalidConfigException; /** * @author Venveo @@ -31,6 +33,7 @@ class Archive extends Model public ?string $uid = null; public ?int $assetId = null; public ?string $hash = null; + public ?string $filename = null; public ?DateTime $dateUpdated; public ?DateTime $dateCreated; @@ -74,22 +77,20 @@ public function getAsset($siteId = null): ?Asset /** * @return string|null + * @throws InvalidConfigException */ public function getLazyLink(): ?string { - if ($this->asset instanceof Asset) { - // Ensure we _can_ get a url for the asset - $assetUrl = $this->asset->getUrl(); - if($assetUrl) { - return $assetUrl; - } - } return UrlHelper::actionUrl('compress/compress/get-link', ['uid' => $this->uid]); } - public function getContents() + /** + * Returns an AssetQuery configured to include the assets within this archive + * @return \craft\elements\db\AssetQuery + */ + public function getContents(): AssetQuery { - return Plugin::$plugin->compress->getArchiveContents($this); + return Plugin::getInstance()->compress->getArchiveContents($this); } /** @@ -101,7 +102,7 @@ public function isReady(): bool if (!$this->assetId) { return false; } - if ($this->getAsset() instanceof Asset) { + if ($this->getAsset()) { return true; } diff --git a/src/models/Settings.php b/src/models/Settings.php index b2e085e..e68cf85 100644 --- a/src/models/Settings.php +++ b/src/models/Settings.php @@ -23,15 +23,34 @@ class Settings extends Model // ========================================================================= /** - * @var string + * The handle for the volume where archives are stored */ - public $defaultVolumeHandle = ''; + public string $defaultVolumeHandle = ''; - public $autoRegenerate = true; + /** + * An optional subdirectory to put zipped files in + */ + public ?string $defaultVolumeSubdirectory = ''; + + /** + * Should we automatically regenerate + */ + public bool $autoRegenerate = true; + + /** + * How many hours do we wait before an archive is considered stale? + */ + public int $deleteStaleArchivesHours = 0; - public $maxFileSize = 0; + /** + * The maximum sum of input files we can compress. Set to 0 for no limit + */ + public int $maxFileSize = 0; - public $maxFileCount = 0; + /** + * The maximum number of input files we can compress. Set to 0 for no limit + */ + public int $maxFileCount = 0; /** * @inheritdoc @@ -39,9 +58,9 @@ class Settings extends Model public function rules(): array { return [ - ['defaultVolumeHandle', 'string'], + [['defaultVolumeHandle', 'defaultVolumeSubdirectory'], 'string'], [['autoRegenerate'], 'boolean'], - [['maxFileSize', 'maxFileCount'], 'integer'], + [['maxFileSize', 'maxFileCount', 'deleteStaleArchivesHours'], 'integer'], ]; } } diff --git a/src/records/Archive.php b/src/records/Archive.php index a42f118..57e73d3 100644 --- a/src/records/Archive.php +++ b/src/records/Archive.php @@ -16,6 +16,7 @@ * @property mixed $fileAssets * @property integer id * @property integer assetId + * @property string filename * @property \DateTime dateLastAccessed * @property string hash */ @@ -46,10 +47,8 @@ protected function prepareForDb(): void { parent::prepareForDb(); $now = Db::prepareDateForDb(new DateTime()); - if ($this->getIsNewRecord()) { - if (!isset($this->dateLastAccessed)) { - $this->dateLastAccessed = $now; - } + if ($this->getIsNewRecord() && !isset($this->dateLastAccessed)) { + $this->dateLastAccessed = $now; } } } diff --git a/src/services/Compress.php b/src/services/Compress.php index 2dbd471..c95c01b 100644 --- a/src/services/Compress.php +++ b/src/services/Compress.php @@ -14,7 +14,11 @@ use craft\base\Component; use craft\elements\Asset; use craft\elements\db\AssetQuery; +use craft\helpers\App; +use craft\helpers\ArrayHelper; use craft\helpers\DateTimeHelper; +use craft\helpers\Db; +use craft\helpers\FileHelper; use craft\helpers\StringHelper; use craft\models\Volume; use venveo\compress\Compress as Plugin; @@ -24,6 +28,8 @@ use venveo\compress\models\Archive as ArchiveModel; use venveo\compress\records\Archive as ArchiveRecord; use venveo\compress\records\File as FileRecord; +use yii\db\Exception; +use yii\db\StaleObjectException; use ZipArchive; @@ -52,7 +58,7 @@ class Compress extends Component * @param null $filename * @return ArchiveModel|null */ - public function getArchiveModelForQuery($query, $lazy = false, $filename = null) + public function getArchiveModelForQuery($query, $lazy = false, $filename = null): ?ArchiveModel { // Get the assets and create a unique hash to represent them if ($query instanceof AssetQuery) { @@ -64,19 +70,18 @@ public function getArchiveModelForQuery($query, $lazy = false, $filename = null) return null; } - $hash = $this->getHashForAssets($assets); + $hash = $this->getHashForAssets($assets, $filename); // Make sure we haven't already hashed these assets. If so, return the // archive. $record = $this->getArchiveRecordByHash($hash); - if ($record instanceof ArchiveRecord && isset($record->assetId)) { - $asset = Craft::$app->assets->getAssetById($record->assetId); + if ($record && $record->assetId && $asset = Craft::$app->assets->getAssetById($record->assetId)) { return ArchiveModel::hydrateFromRecord($record, $asset); } // No existing record, let's create a new one if (!$record instanceof ArchiveRecord) { - $record = $this->createArchiveRecord($assets, null); + $record = $this->createArchiveRecord($assets, null, $filename); } // We'll use the cache to keep track of the status of the archive to @@ -101,16 +106,16 @@ public function getArchiveModelForQuery($query, $lazy = false, $filename = null) // We'll do it live! try { - return $this->createArchiveAsset($record, $filename); + return $this->createArchiveAsset($record); } catch (\Exception $e) { Craft::error($e->getMessage(), __METHOD__); return null; } } - public function createArchiveRecord($assets, $archiveAsset = null) + public function createArchiveRecord($assets, $archiveAsset = null, ?string $filename = null): ArchiveRecord { - $archive = $this->createArchiveRecords($assets, $archiveAsset); + $archive = $this->createArchiveRecords($assets, $archiveAsset, null, $filename); return $archive; } @@ -135,10 +140,15 @@ public function createArchiveAsset(ArchiveRecord $archiveRecord): ?ArchiveModel $assetQuery->id($assetIds); $assets = $assetQuery->all(); $assetName = $uuid . '.zip'; - $filename = $uuid . '.zip'; - // Create the SupportAttachment zip - $zipPath = Craft::$app->getPath()->getTempPath() . DIRECTORY_SEPARATOR . $filename; + if ($archiveRecord->filename) { + $assetName = $archiveRecord->filename . '.zip'; + } + $tempFileName = $uuid . '.zip'; + $tempFileName = \craft\helpers\FileHelper::sanitizeFilename($tempFileName, ['separator' => null]); + $tempDirectory = Craft::$app->getPath()->getTempPath() . DIRECTORY_SEPARATOR . 'compress'; + FileHelper::createDirectory($tempDirectory); + $zipPath = $tempDirectory . DIRECTORY_SEPARATOR . $tempFileName; try { // Create the zip $zip = new ZipArchive(); @@ -147,19 +157,20 @@ public function createArchiveAsset(ArchiveRecord $archiveRecord): ?ArchiveModel throw new CompressException('Cannot create zip file at: ' . $zipPath); } - $maxFileCount = Plugin::$plugin->getSettings()->maxFileCount; + $maxFileCount = Plugin::getInstance()->getSettings()->maxFileCount; if ($maxFileCount > 0 && count($assets) > $maxFileCount) { throw new CompressException('Cannot create zip; maxFileCount exceeded.'); } - $totalFileSize = 0; - $maxFileSize = Plugin::$plugin->getSettings()->maxFileSize; + $totalFileSize = array_reduce($assets, static fn($carry, $asset) => $carry + $asset->size, 0); + $maxFileSize = Plugin::getInstance()->getSettings()->maxFileSize; + if ($maxFileSize > 0 && $totalFileSize > $maxFileSize) { + throw new CompressException('Cannot create zip; maxFileSize exceeded.'); + } + App::maxPowerCaptain(); + foreach ($assets as $asset) { - $totalFileSize += $asset->size; - if ($maxFileSize > 0 && $totalFileSize > $maxFileSize) { - throw new CompressException('Cannot create zip; maxFileSize exceeded.'); - } - $zip->addFile($asset->getCopyOfFile(), $asset->filename); + $zip->addFromString($asset->filename, $asset->getContents()); } $zip->close(); @@ -171,13 +182,18 @@ public function createArchiveAsset(ArchiveRecord $archiveRecord): ?ArchiveModel } $stream = fopen($zipPath, 'rb'); - $volumeHandle = Plugin::$plugin->getSettings()->defaultVolumeHandle; + $volumeHandle = Plugin::getInstance()->getSettings()->defaultVolumeHandle; + $volumeSubdirectory = Plugin::getInstance()->getSettings()->defaultVolumeSubdirectory; /** @var Volume $volume */ $volume = Craft::$app->volumes->getVolumeByHandle($volumeHandle); if (!$volume instanceof Volume) { throw new CompressException('Default volume not set.'); } $finalFilePath = $assetName; + if ($volumeSubdirectory) { + $finalFilePath = $volumeSubdirectory . DIRECTORY_SEPARATOR . $finalFilePath; + } + $finalFilePath = FileHelper::normalizePath($finalFilePath); $fs = $volume->getFs(); $fs->writeFileFromStream($finalFilePath, $stream, []); unlink($zipPath); @@ -190,22 +206,23 @@ public function createArchiveAsset(ArchiveRecord $archiveRecord): ?ArchiveModel } /** - * @param $zippedAssets - * @param $asset - * @param null $archiveRecord + * @param array $zippedAssets + * @param Asset $asset + * @param ArchiveRecord|null $archiveRecord + * @param string|null $filename * @return ArchiveRecord - * @throws \craft\errors\SiteNotFoundException - * @throws \yii\base\Exception - * @throws \yii\base\InvalidConfigException - * @throws \yii\db\Exception + * @throws Exception */ - private function createArchiveRecords($zippedAssets, $asset, $archiveRecord = null) + private function createArchiveRecords(array $zippedAssets, ?Asset $asset = null, ?ArchiveRecord $archiveRecord = null, ?string $filename = null): ArchiveRecord { - if (!$archiveRecord instanceof ArchiveRecord) { + if (!$archiveRecord) { $archiveRecord = new ArchiveRecord(); $archiveRecord->dateLastAccessed = DateTimeHelper::currentUTCDateTime(); $archiveRecord->assetId = $asset->id ?? null; - $archiveRecord->hash = $this->getHashForAssets($zippedAssets); + $archiveRecord->hash = $this->getHashForAssets($zippedAssets, $filename); + if ($filename) { + $archiveRecord->filename = FileHelper::sanitizeFilename($filename, ['separator' => null]); + } $archiveRecord->save(); } @@ -240,10 +257,11 @@ private function createArchiveRecords($zippedAssets, $asset, $archiveRecord = nu /** * Creates a hash * - * @param $assets Asset[] + * @param array $assets Asset + * @param string|null $filename * @return string */ - private function getHashForAssets($assets): string + private function getHashForAssets(array $assets, ?string $filename = null): string { $ids = []; foreach ($assets as $asset) { @@ -253,6 +271,10 @@ private function getHashForAssets($assets): string } sort($ids); $hashKey = implode('', $ids); + + if ($filename) { + $hashKey = $filename . $hashKey; + } return md5($hashKey); } @@ -262,7 +284,7 @@ private function getHashForAssets($assets): string * @param $hash * @return ArchiveRecord|null */ - private function getArchiveRecordByHash($hash) + private function getArchiveRecordByHash($hash): ?ArchiveRecord { return ArchiveRecord::findOne(['hash' => $hash]); } @@ -272,7 +294,7 @@ private function getArchiveRecordByHash($hash) * * @param Asset $asset */ - public function handleAssetUpdated(Asset $asset) + public function handleAssetUpdated(Asset $asset): void { // Get the files this affects and the archives. We're just going to // delete the asset for the archive to prompt it to regenerate. @@ -292,12 +314,13 @@ public function handleAssetUpdated(Asset $asset) try { Craft::$app->elements->deleteElementById($archiveAsset); } catch (\Throwable $e) { - Craft::error('Failed to delete an archive asset after a dependent file was deleted: ' . $e->getMessage(), __METHOD__); + Craft::error('Failed to delete an archive asset after a dependent file was deleted: ' . $e->getMessage(), + __METHOD__); Craft::error($e->getTraceAsString(), __METHOD__); } } - if (Plugin::$plugin->getSettings()->autoRegenerate) { + if (Plugin::getInstance()->getSettings()->autoRegenerate) { foreach ($archiveRecordUids as $recordUid) { $cacheKey = 'Compress:InQueue:' . $recordUid; // Make sure we don't run more than one job for the archive @@ -323,18 +346,13 @@ public function handleAssetUpdated(Asset $asset) public function getArchiveContents(ArchiveModel $archive): AssetQuery { $records = FileRecord::find()->where(['=', 'archiveId', $archive->id])->select(['assetId'])->asArray()->all(); - // There has to be a better way to do this... - $ids = []; - /** FileRecord $record */ - foreach ($records as $record) { - $ids[] = $record['assetId']; - } - return (new AssetQuery(Asset::class))->id($ids); + $ids = ArrayHelper::getColumn($records, 'assetId'); + return Asset::find()->id($ids); } /** - * @param int $offset - * @param null $limit + * @param int|null $offset + * @param int|null $limit * @return array */ public function getArchives(?int $offset = 0, ?int $limit = null): array @@ -369,4 +387,36 @@ public function getArchiveModelByUID($uid): ?ArchiveModel return ArchiveModel::hydrateFromRecord($record); } + + /** + * Deletes registered 404s that haven't been hit in a while + * @param null $limit + * @throws Throwable + * @throws StaleObjectException + */ + public function deleteStaleArchives($limit = null): void + { + $hours = Plugin::getInstance()->getSettings()->deleteStaleArchivesHours; + + $interval = DateTimeHelper::secondsToInterval($hours * 60 * 60); + $expire = DateTimeHelper::currentUTCDateTime(); + $pastTime = $expire->sub($interval); + + $query = ArchiveRecord::find() + ->andWhere(['<', 'dateLastAccessed', Db::prepareDateForDb($pastTime)]); + + if ($limit) { + $query->limit($limit); + } + $archives = $query->all(); + foreach($archives as $archive) { + $assetId = $archive->assetId; + if ($assetId) { + Craft::$app->elements->deleteElementById($assetId, null, null, true); + } + $archive->delete(); + } + } + + } diff --git a/src/templates/settings.twig b/src/templates/settings.twig index bafa2d9..73de44a 100644 --- a/src/templates/settings.twig +++ b/src/templates/settings.twig @@ -24,11 +24,20 @@ {{ forms.selectField({ first: true, label: 'Default Volume'|t('compress'), - instructions: 'Select the volume for archive storage'|t('compress'), + instructions: 'When the Compress plugin is used, the ephemeral archives generated will be stored in this volume.'|t('compress'), id: 'defaultVolumeHandle', name: 'defaultVolumeHandle', - value: settings['defaultVolumeHandle'], - options: options + value: settings.defaultVolumeHandle, + options: options, +}) }} + +{{ forms.textField({ + name: 'defaultVolumeSubdirectory', + label: 'Default Upload Location'|t('compress'), + value: settings.defaultVolumeSubdirectory, + instructions: "Where archive assets should be stored (relative to **Default Volume**). If this volume doesn't have public URLs, the archives will be proxied through the server to the requester."|t('compress'), + placeholder: 'path/to/subfolder'|t('compress'), + errors: settings.getErrors('defaultVolumeSubdirectory') }) }} {{ forms.textField({ @@ -58,4 +67,15 @@ label: 'Auto-Regenerate'|t('compress'), instructions: 'When enabled, Compress will automatically regenerate archives when one of its dependent files is changed or removed.'|t('compress'), on: settings['autoRegenerate'], - }) }} \ No newline at end of file + }) }} + + +{{ forms.textField({ + type: 'number', + label: "How long to wait before an archive is considered stale"|t('compress'), + instructions: "Enter how many hours to wait before a generated archive is considered stale and can be deleted."|t('compress'), + id: 'deleteStaleArchivesHours', + name: 'deleteStaleArchivesHours', + value: settings.deleteStaleArchivesHours, + errors: settings.getErrors('deleteStaleArchivesHours') +}) }} diff --git a/src/variables/CompressVariable.php b/src/variables/CompressVariable.php index a0baf91..74fa46f 100644 --- a/src/variables/CompressVariable.php +++ b/src/variables/CompressVariable.php @@ -27,7 +27,7 @@ class CompressVariable * @param bool $lazy * @return ArchiveModel|null */ - public function zip($query, $lazy = false, $filename = null) + public function zip($query, $lazy = false, $filename = null): ?ArchiveModel { return Compress::$plugin->compress->getArchiveModelForQuery($query, $lazy, $filename); }