11<?php
22
33declare (strict_types=1 );
4- /**
5- * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
4+
5+ /*
6+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH
7+ * SPDX-FileContributor: Carl Schwan
68 * SPDX-License-Identifier: AGPL-3.0-or-later
79 */
810
1719use OCP \DB \Exception ;
1820use OCP \Files \AppData \IAppDataFactory ;
1921use OCP \Files \IAppData ;
20- use OCP \Files \SimpleFS \ISimpleFile ;
22+ use OCP \Files \IRootFolder ;
23+ use OCP \Files \NotFoundException ;
2124use OCP \Files \SimpleFS \ISimpleFolder ;
2225use OCP \IAppConfig ;
2326use OCP \IDBConnection ;
@@ -28,10 +31,11 @@ class MovePreviewJob extends TimedJob {
2831
2932 public function __construct (
3033 ITimeFactory $ time ,
31- private IAppConfig $ appConfig ,
32- private PreviewMapper $ previewMapper ,
33- private StorageFactory $ storageFactory ,
34- private IDBConnection $ connection ,
34+ private readonly IAppConfig $ appConfig ,
35+ private readonly PreviewMapper $ previewMapper ,
36+ private readonly StorageFactory $ storageFactory ,
37+ private readonly IDBConnection $ connection ,
38+ private readonly IRootFolder $ rootFolder ,
3539 IAppDataFactory $ appDataFactory ,
3640 ) {
3741 parent ::__construct ($ time );
@@ -42,15 +46,6 @@ public function __construct(
4246 }
4347
4448 protected function run (mixed $ argument ): void {
45- try {
46- $ this ->doRun ($ argument );
47- } catch (\Throwable $ exception ) {
48- echo $ exception ->getMessage ();
49- throw $ exception ;
50- }
51- }
52-
53- private function doRun ($ argument ): void {
5449 if ($ this ->appConfig ->getValueBool ('core ' , 'previewMovedDone ' )) {
5550 return ;
5651 }
@@ -59,27 +54,21 @@ private function doRun($argument): void {
5954
6055 $ startTime = time ();
6156 while (true ) {
62- $ previewFolders = [];
63-
6457 // Check new hierarchical preview folders first
6558 if (!$ emptyHierarchicalPreviewFolders ) {
6659 $ qb = $ this ->connection ->getQueryBuilder ();
6760 $ qb ->select ('* ' )
6861 ->from ('filecache ' )
6962 ->where ($ qb ->expr ()->like ('path ' , $ qb ->createNamedParameter ('appdata_%/preview/%/%/%/%/%/%/%/%/% ' )))
63+ ->hintShardKey ('storage ' , $ this ->rootFolder ->getMountPoint ()->getNumericStorageId ())
7064 ->setMaxResults (100 );
7165
7266 $ result = $ qb ->executeQuery ();
7367 while ($ row = $ result ->fetch ()) {
7468 $ pathSplit = explode ('/ ' , $ row ['path ' ]);
7569 assert (count ($ pathSplit ) >= 2 );
7670 $ fileId = $ pathSplit [count ($ pathSplit ) - 2 ];
77- $ previewFolders [$ fileId ][] = $ row ['path ' ];
78- }
79-
80- if (!empty ($ previewFolders )) {
81- $ this ->processPreviews ($ previewFolders , false );
82- continue ;
71+ $ this ->processPreviews ($ fileId , false );
8372 }
8473 }
8574
@@ -89,6 +78,7 @@ private function doRun($argument): void {
8978 $ qb ->select ('* ' )
9079 ->from ('filecache ' )
9180 ->where ($ qb ->expr ()->like ('path ' , $ qb ->createNamedParameter ('appdata_%/preview/%/%.% ' )))
81+ ->hintShardKey ('storage ' , $ this ->rootFolder ->getMountPoint ()->getNumericStorageId ())
9282 ->setMaxResults (100 );
9383
9484 $ result = $ qb ->executeQuery ();
@@ -98,18 +88,7 @@ private function doRun($argument): void {
9888 $ fileId = $ pathSplit [count ($ pathSplit ) - 2 ];
9989 array_pop ($ pathSplit );
10090 $ path = implode ('/ ' , $ pathSplit );
101- if (!isset ($ previewFolders [$ fileId ])) {
102- $ previewFolders [$ fileId ] = [];
103- }
104- if (!in_array ($ path , $ previewFolders [$ fileId ])) {
105- $ previewFolders [$ fileId ][] = $ path ;
106- }
107- }
108-
109- if (empty ($ previewFolders )) {
110- break ;
111- } else {
112- $ this ->processPreviews ($ previewFolders , true );
91+ $ this ->processPreviews ($ fileId , true );
11392 }
11493
11594 // Stop if execution time is more than one hour.
@@ -118,97 +97,114 @@ private function doRun($argument): void {
11897 }
11998 }
12099
121- // Delete any leftover preview directory
122- $ this ->appData ->getFolder ('. ' )->delete ();
100+ try {
101+ // Delete any leftover preview directory
102+ $ this ->appData ->getFolder ('. ' )->delete ();
103+ } catch (NotFoundException ) {
104+ // ignore
105+ }
123106 $ this ->appConfig ->setValueBool ('core ' , 'previewMovedDone ' , true );
124107 }
125108
126109 /**
127110 * @param array<string|int, string[]> $previewFolders
128111 */
129- private function processPreviews (array $ previewFolders , bool $ simplePaths ): void {
130- foreach ($ previewFolders as $ fileId => $ previewFolder ) {
131- $ internalPath = $ this ->getInternalFolder ((string )$ fileId , $ simplePaths );
132- $ folder = $ this ->appData ->getFolder ($ internalPath );
133-
134- /**
135- * @var list<array{file: SimpleFile, width: int, height: int, crop: bool, max: bool, extension: string, mtime: int, size: int}> $previewFiles
136- */
137- $ previewFiles = [];
138-
139- foreach ($ folder ->getDirectoryListing () as $ previewFile ) {
140- /** @var SimpleFile $previewFile */
141- [0 => $ baseName , 1 => $ extension ] = explode ('. ' , $ previewFile ->getName ());
142- $ nameSplit = explode ('- ' , $ baseName );
143-
144- // TODO VERSION/PREFIX extraction
145-
146- $ width = $ nameSplit [0 ];
147- $ height = $ nameSplit [1 ];
112+ private function processPreviews (int |string $ fileId , bool $ simplePaths ): void {
113+ $ internalPath = $ this ->getInternalFolder ((string )$ fileId , $ simplePaths );
114+ $ folder = $ this ->appData ->getFolder ($ internalPath );
115+
116+ /**
117+ * @var list<array{
118+ * file: SimpleFile, width: int, height: int, crop: bool, max: bool, extension: string, mtime: int, size: int, version: ?int
119+ * }> $previewFiles
120+ */
121+ $ previewFiles = [];
122+
123+ foreach ($ folder ->getDirectoryListing () as $ previewFile ) {
124+ /** @var SimpleFile $previewFile */
125+ [0 => $ baseName , 1 => $ extension ] = explode ('. ' , $ previewFile ->getName ());
126+ $ nameSplit = explode ('- ' , $ baseName );
127+
128+ $ offset = 0 ;
129+ $ version = null ;
130+ if (count ($ nameSplit ) === 4 || (count ($ nameSplit ) === 3 && is_numeric ($ nameSplit [2 ]))) {
131+ $ offset = 1 ;
132+ $ version = (int )$ nameSplit [0 ];
133+ }
148134
149- if (isset ($ nameSplit [2 ])) {
150- $ crop = $ nameSplit [2 ] === 'crop ' ;
151- $ max = $ nameSplit [2 ] === 'max ' ;
152- }
135+ $ width = (int )$ nameSplit [$ offset + 0 ];
136+ $ height = (int )$ nameSplit [$ offset + 1 ];
153137
154- $ previewFiles [] = [
155- 'file ' => $ previewFile ,
156- 'width ' => $ width ,
157- 'height ' => $ height ,
158- 'crop ' => $ crop ,
159- 'max ' => $ max ,
160- 'extension ' => $ extension ,
161- 'size ' => $ previewFile ->getSize (),
162- 'mtime ' => $ previewFile ->getMTime (),
163- ];
138+ $ crop = false ;
139+ $ max = false ;
140+ if (isset ($ nameSplit [$ offset + 2 ])) {
141+ $ crop = $ nameSplit [$ offset + 2 ] === 'crop ' ;
142+ $ max = $ nameSplit [$ offset + 2 ] === 'max ' ;
164143 }
165144
166- $ qb = $ this ->connection ->getQueryBuilder ();
167- $ qb ->select ('* ' )
168- ->from ('filecache ' )
169- ->where ($ qb ->expr ()->like ('fileid ' , $ qb ->createNamedParameter ($ fileId )));
145+ $ previewFiles [] = [
146+ 'file ' => $ previewFile ,
147+ 'width ' => $ width ,
148+ 'height ' => $ height ,
149+ 'crop ' => $ crop ,
150+ 'version ' => $ version ,
151+ 'max ' => $ max ,
152+ 'extension ' => $ extension ,
153+ 'size ' => $ previewFile ->getSize (),
154+ 'mtime ' => $ previewFile ->getMTime (),
155+ ];
156+ }
170157
171- $ result = $ qb ->executeQuery ();
172- $ result = $ result ->fetchAll ();
173-
174- if (count ($ result ) > 0 ) {
175- foreach ($ previewFiles as $ previewFile ) {
176- $ preview = new Preview ();
177- $ preview ->setFileId ((int )$ fileId );
178- $ preview ->setOldFileId ($ previewFile ['file ' ]->getId ());
179- $ preview ->setEtag ($ result [0 ]['etag ' ]);
180- $ preview ->setMtime ($ previewFile ['mtime ' ]);
181- $ preview ->setWidth ($ previewFile ['width ' ]);
182- $ preview ->setHeight ($ previewFile ['height ' ]);
183- $ preview ->setCrop ($ previewFile ['crop ' ]);
184- $ preview ->setIsMax ($ previewFile ['max ' ]);
185- $ preview ->setMimetype (match ($ previewFile ['extension ' ]) {
186- 'png ' => IPreview::MIMETYPE_PNG ,
187- 'webp ' => IPreview::MIMETYPE_WEBP ,
188- 'gif ' => IPreview::MIMETYPE_GIF ,
189- default => IPreview::MIMETYPE_JPEG ,
190- });
191- $ preview ->setSize ($ previewFile ['size ' ]);
192- try {
193- $ preview = $ this ->previewMapper ->insert ($ preview );
194- } catch (Exception $ e ) {
195- // We already have this preview in the preview table, skip
196- continue ;
197- }
198-
199- try {
200- $ this ->storageFactory ->migratePreview ($ preview , $ previewFile ['file ' ]);
201- $ previewFile ['file ' ]->delete ();
202- } catch (\Exception $ e ) {
203- $ this ->previewMapper ->delete ($ preview );
204- throw $ e ;
205- }
158+ $ qb = $ this ->connection ->getQueryBuilder ();
159+ $ qb ->select ('* ' )
160+ ->from ('filecache ' )
161+ ->where ($ qb ->expr ()->eq ('fileid ' , $ qb ->createNamedParameter ($ fileId )))
162+ ->runAcrossAllShards (); // Unavoidable because we can just extract the file_id in the preview name
163+
164+ $ result = $ qb ->executeQuery ();
165+ $ result = $ result ->fetchAll ();
166+
167+ if (count ($ result ) > 0 ) {
168+ foreach ($ previewFiles as $ previewFile ) {
169+ $ preview = new Preview ();
170+ $ preview ->setFileId ((int )$ fileId );
171+ /** @var SimpleFile $file */
172+ $ file = $ previewFile ['file ' ];
173+ $ preview ->setOldFileId ($ file ->getId ());
174+ $ preview ->setStorageId ($ result [0 ]['storage ' ]);
175+ $ preview ->setEtag ($ result [0 ]['etag ' ]);
176+ $ preview ->setMtime ($ previewFile ['mtime ' ]);
177+ $ preview ->setWidth ($ previewFile ['width ' ]);
178+ $ preview ->setHeight ($ previewFile ['height ' ]);
179+ $ preview ->setCropped ($ previewFile ['crop ' ]);
180+ $ preview ->setVersion ($ previewFile ['version ' ]);
181+ $ preview ->setMax ($ previewFile ['max ' ]);
182+ $ preview ->setEncrypted (false );
183+ $ preview ->setMimetype (match ($ previewFile ['extension ' ]) {
184+ 'png ' => IPreview::MIMETYPE_PNG ,
185+ 'webp ' => IPreview::MIMETYPE_WEBP ,
186+ 'gif ' => IPreview::MIMETYPE_GIF ,
187+ default => IPreview::MIMETYPE_JPEG ,
188+ });
189+ $ preview ->setSize ($ previewFile ['size ' ]);
190+ try {
191+ $ preview = $ this ->previewMapper ->insert ($ preview );
192+ } catch (Exception $ e ) {
193+ // We already have this preview in the preview table, skip
194+ continue ;
195+ }
206196
197+ try {
198+ $ this ->storageFactory ->migratePreview ($ preview , $ previewFile ['file ' ]);
199+ $ previewFile ['file ' ]->delete ();
200+ } catch (\Exception $ e ) {
201+ $ this ->previewMapper ->delete ($ preview );
202+ throw $ e ;
207203 }
208204 }
209-
210- $ this ->deleteFolder ($ internalPath , $ folder );
211205 }
206+
207+ $ this ->deleteFolder ($ internalPath , $ folder );
212208 }
213209
214210 public static function getInternalFolder (string $ name , bool $ simplePaths ): string {
0 commit comments