diff --git a/appinfo/info.xml b/appinfo/info.xml
index 003e21c..ebef68d 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -20,4 +20,7 @@
+
+ OCA\CameraRawPreviews\Command\DeletePreviews
+
diff --git a/lib/Command/DeletePreviews.php b/lib/Command/DeletePreviews.php
new file mode 100644
index 0000000..042236d
--- /dev/null
+++ b/lib/Command/DeletePreviews.php
@@ -0,0 +1,257 @@
+appName = $appName;
+ $this->config = $config;
+ $this->connection = $connection;
+ $this->encryptionManager = $encryptionManager;
+ $this->instanceId = $this->config->getSystemValue('instanceid');
+ $this->dataDirectory = $this->config->getSystemValue('datadirectory');
+ }
+
+ protected function configure()
+ {
+ $this
+ ->setName('camerarawpreviews:delete-previews')
+ ->setDescription('Delete generated previews by the CameraRawPreviews')
+ ->addOption(
+ 'mime',
+ 'm',
+ InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
+ 'Specify mime types to delete. Default --mime=image/x-dcraw --mime=image/x-indesign'
+ )
+ ->addOption(
+ 'force',
+ 'f',
+ InputOption::VALUE_NONE,
+ 'Needed to delete files'
+ );
+ }
+
+ /**
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @return int
+ * @throws Exception
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ if ($this->encryptionManager->isEnabled()) {
+ $output->writeln('This command does work with encryption.');
+ return 1;
+ }
+
+ $mimes = $input->getOption('mime');
+ if (empty($mimes)) {
+ $mimes = ['image/x-dcraw', 'image/x-indesign'];
+ }
+
+ $dryRun = !$input->getOption('force');
+ $this->output = $output;
+ $count = $this->work($dryRun, $mimes);
+
+ if ($count > 1) {
+ $output->writeln("Deleted previews for $count file(s).");
+ } else if ($count === 1) {
+ $output->writeln("Deleted previews for $count file(s).");
+ } else {
+ $output->writeln("Nothing to delete.");
+ }
+
+ return 0;
+ }
+
+ private function work(bool $dryRun, array $mimes)
+ {
+ $appdataFolder = 'appdata_' . $this->instanceId;
+ $previewFolder = $appdataFolder . '/preview';
+ $lastFolderId = null;
+ $deleteCount = 0;
+ $previewCount = null;
+
+ while (true) {
+
+ $qb = $this->connection->getQueryBuilder();
+ $qb
+ ->select('c2.fileid', 'c2.storage', 'c2.path')
+ ->from('filecache', 'c1')
+ ->from('filecache', 'c2')
+ ->from('mimetypes', 'm')
+ ->where($qb->expr()->in('m.mimetype', $qb->createNamedParameter($mimes, $qb::PARAM_STR_ARRAY)))
+ ->andWhere($qb->expr()->eq('m.id', 'c1.mimetype'))
+ ->andWhere($qb->expr()->eq('c2.name', 'c1.fileid'))
+ ->andWhere($qb->expr()->like('c2.path', $qb->createNamedParameter($previewFolder . '/%', $qb::PARAM_STR)))
+ ->setFirstResult($deleteCount)
+ ->setMaxResults(100);
+
+
+ $stmt = $qb->executeQuery();
+ $previewFolders = $stmt->fetchAll();
+ $stmt->closeCursor();
+
+ if (count($previewFolders) === 0) {
+ break;
+ }
+
+ foreach ($previewFolders as &$folder) {
+ if ($lastFolderId === $folder['fileid']) {
+ return;
+ }
+ $lastFolderId = $folder['fileid'];
+
+ $re = '/^appdata_\w+\/preview\/.+\/(\d+)$/';
+
+ if (!preg_match($re, $folder['path'], $matches)) {
+ throw new Exception('Could not extract out file id for preview: ' . json_encode($folder));
+ }
+ $originalImageFileId = $matches[1];
+ $qb = $this->connection->getQueryBuilder();
+ $qb->select('path')
+ ->from('filecache')
+ ->where(
+ $qb->expr()->eq('fileid', $qb->createNamedParameter($originalImageFileId))
+ );
+ $cursor = $qb->executeQuery();
+ $originalImageFile = $cursor->fetch();
+ $cursor->closeCursor();
+
+ if ($originalImageFile === false) {
+ continue;
+ }
+
+ $deleteCount++;
+
+ if ($dryRun) {
+ $this->output->writeln("DRY RUN: Successfully deleted all previews for $originalImageFile[path]");
+ continue;
+ }
+
+ if ($this->deletePreviews($folder)) {
+ $this->output->writeln("Successfully deleted all previews for $originalImageFile[path]");
+ }
+ }
+ }
+
+ return $deleteCount;
+ }
+
+ private function deletePreviews($folder): bool
+ {
+ $folderDirectory = "$this->dataDirectory/$folder[path]";
+
+ if (!file_exists($folderDirectory)) {
+ throw new NotFoundException("$folder[path] is not there.");
+ }
+
+ if (!is_writable($folderDirectory)) {
+ $this->output->writeln("folder [$folderDirectory] is not writeable.");
+ return false;
+ }
+
+ $qb = $this->connection->getQueryBuilder();
+ $qb->select('*')
+ ->from('filecache')
+ ->where($qb->expr()->eq('parent', $qb->createNamedParameter($folder['fileid'])))
+ ->andWhere($qb->expr()->like('path', $qb->createNamedParameter("$folder[path]/%")))
+ ->andWhere($qb->expr()->eq('storage', $qb->createNamedParameter($folder['storage'])));
+
+ $cursor = $qb->executeQuery();
+ $previews = $cursor->fetchAll();
+ $cursor->closeCursor();
+ foreach ($previews as $preview) {
+ $previewRealPath = "$this->dataDirectory/$preview[path]";
+
+ if (file_exists($previewRealPath) && !is_writable($previewRealPath)) {
+ $this->output->writeln("$previewRealPath is not writeable.");
+ return false;
+ }
+
+ $this->connection->beginTransaction();
+ try {
+ $qb->delete('filecache')
+ ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($preview['fileid'])));
+ $qb->executeStatement();
+ if (!file_exists($previewRealPath)) {
+ throw new NotFoundException("$preview[path] is not there.");
+ }
+ if (!unlink($previewRealPath)) {
+ throw new Exception("Could not delete $preview[path]");
+ }
+ $this->connection->commit();
+ } catch (Exception $e) {
+ $this->output->writeln('' . $e->getMessage() . '');
+ $this->connection->rollBack();
+ return false;
+ }
+
+ }
+
+ $this->connection->beginTransaction();
+ try {
+ $qb->delete('filecache')
+ ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($folder['fileid'])));
+ $qb->executeStatement();
+ if (!rmdir($folderDirectory)) {
+ throw new Exception("Could not delete $folder[path]");
+ }
+ $this->connection->commit();
+ } catch (Exception $e) {
+ $this->output->writeln('' . $e->getMessage() . '');
+ $this->connection->rollBack();
+ return false;
+ }
+
+ return true;
+ }
+}