<p>Best way to handle this is probably with a <a href="https://docs.craftcms.com/v3/extend/commands.html" rel="nofollow noreferrer">console command</a> provided by a <a href="https://docs.craftcms.com/v3/extend/module-guide.html" rel="nofollow noreferrer">module</a>.</p>

<p>The console command would need to fetch all asset IDs that don’t have any corresponding rows in the <code>relations</code> database table.</p>

<pre class="lang-php prettyprint-override"><code>use Craft;
use craft\db\Query;
use craft\db\Table;
use craft\elements\Asset;
use yii\console\ExitCode;
use craft\helpers\Console;

public function actionPurgeUnusedAssets(): int
{
    // Find any asset IDs that aren't related to anything
    $assetIds = (new Query())
        -&gt;select(['a.id'])
        -&gt;from(['a' =&gt; Table::ASSETS])
        -&gt;leftJoin(Table::RELATIONS . ' r', '[[r.targetId]] = [[a.id]]')
        -&gt;where(['r.id' =&gt; null])
        -&gt;column();

    if (empty($assetIds)) {
        $this-&gt;stdout('No unrelated assets to delete.' . PHP_EOL);
        return ExitCode::OK;
    }

    // Now fetch and delete them
    $assets = Asset::find()
        -&gt;id($assetIds)
        -&gt;all();

    $this-&gt;stdout('Found ' . count($assets) . ' unrelated assets.' . PHP_EOL);

    foreach ($assets as $asset) {
        $this-&gt;stdout(" - Deleting asset {$asset-&gt;filename} ... ");
        Craft::$app-&gt;elements-&gt;deleteElement($asset);
        $this-&gt;stdout('done' . PHP_EOL, Console::FG_GREEN);
    }

    $this-&gt;stdout('Finished deleting unrelated assets.' . PHP_EOL);
    return ExitCode::OK;
}
</code></pre>

<p>Then, you can set up a cron job on your server that hits this command every 24 hours.</p>

<pre><code>0 0 * * * /path/to/project/craft your-module/purge-unused-assets
</code></pre>

CC BY-SA 4.0
2020-02-07T14:47:29.300Z
0 0 * * * /path/to/project/craft your-module/action-purge-unused-assets is it not this or?
Bram Beekman
CC BY-SA 4.0
2020-02-07T15:02:06.810Z
@BramBeekman No, you don't include the `action-` part in the command.
Brandon Kelly
CC BY-SA 4.0
2020-02-08T08:00:47.750Z
$this->stdout(" - Deleting asset {$$asset->filename} ... ");  -> $this->stdout(" - Deleting asset {$asset->filename} ... ");
Bram Beekman
CC BY-SA 4.0
2020-02-08T13:36:15.973Z
@BramBeekman Thanks, fixed.
Brandon Kelly
CC BY-SA 4.0
2021-11-23T09:44:05.617Z
@BrandonKelly the Problem with this solution is that Assets that are "linked" in Redactor fields will be deleted either way and result into dead links.
Leonardo
CC BY-SA 4.0
2021-11-25T09:08:58.553Z
@BrandonKelly Any solution for assets that are linked inside a redactor field?
Nicholas
CC BY-SA 4.0
2020-02-07T13:21:15.200Z
2020-02-08T13:35:57.397Z
2020-02-08T13:35:57.397Z
Brandon Kelly
<p>I once wrote a custom service doing this (not for commerce, though). I'll post the code, and maybe it can help you along the way. You can set it up in a module, make a console controller, and have a cronjob hit it every minute (although, are you sure you need to do it that often?).</p>

<p>The DeleteAssets is a very simple extension of BaseJob, basically looping through the assets, and deleting them one by one, just so it's handled as queue job. You could also loop through and delete each with <code>Craft::$app-&gt;getElements()-&gt;deleteElementById()</code>, but doing it as a queue job is probably better.</p>

<pre class="lang-php prettyprint-override"><code>    public function deleteUnusedAssets() {

        //First fetch all ids from elements table
        $allIds = Entry::find()
                 -&gt;limit(null)
                 -&gt;ids();

        // Query for all related Assets (assets that are used somewhere)
        $usedAssets = Asset::find()
                -&gt;relatedTo($allIds)
                -&gt;limit(null)
                -&gt;ids();

        // Query for all assets NOT in the list of the used ones
        $excludeString = 'and, not ' . implode(', not ', $usedAssets);
        $unUsedAssets = Asset::find()
                -&gt;id($excludeString)
                -&gt;limit(null)
                -&gt;ids();

        $queue = Craft::$app-&gt;getQueue();
        $jobID = $queue-&gt;push(new DeleteAssets([
            'description' =&gt; 'Deleting ' . count($unUsedAssets) . ' assets'),
            'elements' =&gt; $unUsedAssets,
        ]));
        $queue-&gt;run();

    }
</code></pre>

CC BY-SA 4.0
2020-02-06T14:25:32.187Z
Unable to locate message source for category 'cinema-program'.
Bram Beekman
CC BY-SA 4.0
2020-02-09T19:18:39.210Z
Ah, that is the handle for my plugin. Just use your own, or replace description with something that doesn't use the translate function. I'll update the answer.
KSP
CC BY-SA 4.0
2020-02-05T11:42:59.173Z
2020-02-09T19:19:51.547Z
2020-02-09T19:19:51.547Z
KSP
<p>I'm searching for a option to remove all unused images every minute.</p>

<p>The images I use are for Craft Commerce. </p>

<plugin-craftcommerce>
I'm searching for a option to remove all unused images
# I'm searching for a option to remove all unused images

(Asked by: username_0 on 2020-02-05)

I'm searching for a option to remove all unused images every minute.

The images I use are for Craft Commerce.

**Tags:** plugin-craftcommerce

---

## 2 answers

### Answer by username_1 on 2020-02-07. Score: 3 | Accepted answer

Best way to handle this is probably with a console command provided by a module.
The console command would need to fetch all asset IDs that don’t have any corresponding rows in the relations database table.
use Craft;
use craft\db\Query;
use craft\db\Table;
use craft\elements\Asset;
use yii\console\ExitCode;
use craft\helpers\Console;

public function actionPurgeUnusedAssets(): int
{
    // Find any asset IDs that aren't related to anything
    $assetIds = (new Query())
        ->select(['a.id'])
        ->from(['a' => Table::ASSETS])
        ->leftJoin(Table::RELATIONS . ' r', '[[r.targetId]] = [[a.id]]')
        ->where(['r.id' => null])
        ->column();

    if (empty($assetIds)) {
        $this->stdout('No unrelated assets to delete.' . PHP_EOL);
        return ExitCode::OK;
    }

    // Now fetch and delete them
    $assets = Asset::find()
        ->id($assetIds)
        ->all();

    $this->stdout('Found ' . count($assets) . ' unrelated assets.' . PHP_EOL);

    foreach ($assets as $asset) {
        $this->stdout(" - Deleting asset {$asset->filename} ... ");
        Craft::$app->elements->deleteElement($asset);
        $this->stdout('done' . PHP_EOL, Console::FG_GREEN);
    }

    $this->stdout('Finished deleting unrelated assets.' . PHP_EOL);
    return ExitCode::OK;
}

Then, you can set up a cron job on your server that hits this command every 24 hours.
0 0 * * * /path/to/project/craft your-module/purge-unused-assets

> **Comments:**
> **username_0**: 0 0 * * * /path/to/project/craft your-module/action-purge-unused-assets is it not this or?  
> **username_1**: @BramBeekman No, you don't include the `action-` part in the command.  
> **username_0**: $this->stdout(" - Deleting asset {$$asset->filename} ... ");  -> $this->stdout(" - Deleting asset {$asset->filename} ... ");  
> **username_1**: @BramBeekman Thanks, fixed.  
> **username_2**: @BrandonKelly the Problem with this solution is that Assets that are "linked" in Redactor fields will be deleted either way and result into dead links.  
> **username_3**: @BrandonKelly Any solution for assets that are linked inside a redactor field?  

---

### Answer by username_4 on 2020-02-05. Score: 1

I once wrote a custom service doing this (not for commerce, though). I'll post the code, and maybe it can help you along the way. You can set it up in a module, make a console controller, and have a cronjob hit it every minute (although, are you sure you need to do it that often?).
The DeleteAssets is a very simple extension of BaseJob, basically looping through the assets, and deleting them one by one, just so it's handled as queue job. You could also loop through and delete each with Craft::$app->getElements()->deleteElementById(), but doing it as a queue job is probably better.
    public function deleteUnusedAssets() {

        //First fetch all ids from elements table
        $allIds = Entry::find()
                 ->limit(null)
                 ->ids();

        // Query for all related Assets (assets that are used somewhere)
        $usedAssets = Asset::find()
                ->relatedTo($allIds)
                ->limit(null)
                ->ids();

        // Query for all assets NOT in the list of the used ones
        $excludeString = 'and, not ' . implode(', not ', $usedAssets);
        $unUsedAssets = Asset::find()
                ->id($excludeString)
                ->limit(null)
                ->ids();

        $queue = Craft::$app->getQueue();
        $jobID = $queue->push(new DeleteAssets([
            'description' => 'Deleting ' . count($unUsedAssets) . ' assets'),
            'elements' => $unUsedAssets,
        ]));
        $queue->run();

    }

> **Comments:**
> **username_0**: Unable to locate message source for category 'cinema-program'.  
> **username_4**: Ah, that is the handle for my plugin. Just use your own, or replace description with something that doesn't use the translate function. I'll update the answer.  

---

