22
33declare (strict_types=1 );
44
5- /*
5+ /**
66 * SPDX-FileCopyrightText: 2025 Nextcloud GmbH
77 * SPDX-FileContributor: Carl Schwan
88 * SPDX-License-Identifier: AGPL-3.0-or-later
99 */
1010
11- namespace OC \Core \ BackgroundJobs ;
11+ namespace OC \Preview ;
1212
1313use OC \Files \SimpleFS \SimpleFile ;
1414use OC \Preview \Db \Preview ;
1515use OC \Preview \Db \PreviewMapper ;
1616use OC \Preview \Storage \StorageFactory ;
17- use OCP \AppFramework \Utility \ITimeFactory ;
18- use OCP \BackgroundJob \TimedJob ;
1917use OCP \DB \Exception ;
20- use OCP \DB \IResult ;
2118use OCP \Files \AppData \IAppDataFactory ;
2219use OCP \Files \IAppData ;
2320use OCP \Files \IMimeTypeDetector ;
2421use OCP \Files \IMimeTypeLoader ;
2522use OCP \Files \IRootFolder ;
26- use OCP \IAppConfig ;
23+ use OCP \Files \ NotFoundException ;
2724use OCP \IConfig ;
2825use OCP \IDBConnection ;
29- use Override ;
3026use Psr \Log \LoggerInterface ;
3127
32- class MovePreviewJob extends TimedJob {
28+ class PreviewMigrationService {
3329 private IAppData $ appData ;
3430 private string $ previewRootPath ;
3531
3632 public function __construct (
37- ITimeFactory $ time ,
38- private readonly IAppConfig $ appConfig ,
3933 private readonly IConfig $ config ,
40- private readonly PreviewMapper $ previewMapper ,
41- private readonly StorageFactory $ storageFactory ,
42- private readonly IDBConnection $ connection ,
4334 private readonly IRootFolder $ rootFolder ,
35+ private readonly LoggerInterface $ logger ,
4436 private readonly IMimeTypeDetector $ mimeTypeDetector ,
4537 private readonly IMimeTypeLoader $ mimeTypeLoader ,
46- private readonly LoggerInterface $ logger ,
38+ private readonly IDBConnection $ connection ,
39+ private readonly PreviewMapper $ previewMapper ,
40+ private readonly StorageFactory $ storageFactory ,
4741 IAppDataFactory $ appDataFactory ,
4842 ) {
49- parent ::__construct ($ time );
50-
5143 $ this ->appData = $ appDataFactory ->get ('preview ' );
52- $ this ->setTimeSensitivity (self ::TIME_INSENSITIVE );
53- $ this ->setInterval (24 * 60 * 60 );
5444 $ this ->previewRootPath = 'appdata_ ' . $ this ->config ->getSystemValueString ('instanceid ' ) . '/preview/ ' ;
5545 }
5646
57- #[Override]
58- protected function run (mixed $ argument ): void {
59- if ($ this ->appConfig ->getValueBool ('core ' , 'previewMovedDone ' )) {
60- return ;
61- }
62-
63- $ startTime = time ();
64- while (true ) {
65- $ qb = $ this ->connection ->getQueryBuilder ();
66- $ qb ->select ('path ' )
67- ->from ('filecache ' )
68- // Hierarchical preview folder structure
69- ->where ($ qb ->expr ()->like ('path ' , $ qb ->createNamedParameter ($ this ->previewRootPath . '%/%/%/%/%/%/%/%/% ' )))
70- // Legacy flat preview folder structure
71- ->orWhere ($ qb ->expr ()->like ('path ' , $ qb ->createNamedParameter ($ this ->previewRootPath . '%/%.% ' )))
72- ->hintShardKey ('storage ' , $ this ->rootFolder ->getMountPoint ()->getNumericStorageId ())
73- ->setMaxResults (100 );
74-
75- $ result = $ qb ->executeQuery ();
76- $ foundPreviews = $ this ->processQueryResult ($ result );
77-
78- if (!$ foundPreviews ) {
79- break ;
80- }
81-
82- // Stop if execution time is more than one hour.
83- if (time () - $ startTime > 3600 ) {
84- return ;
85- }
86- }
87-
88- $ this ->appConfig ->setValueBool ('core ' , 'previewMovedDone ' , true );
89- }
90-
91- private function processQueryResult (IResult $ result ): bool {
92- $ foundPreview = false ;
93- $ fileIds = [];
94- $ flatFileIds = [];
95- while ($ row = $ result ->fetchAssociative ()) {
96- $ pathSplit = explode ('/ ' , $ row ['path ' ]);
97- assert (count ($ pathSplit ) >= 2 );
98- $ fileId = (int )$ pathSplit [count ($ pathSplit ) - 2 ];
99- if (count ($ pathSplit ) === 11 ) {
100- // Hierarchical structure
101- if (!in_array ($ fileId , $ fileIds )) {
102- $ fileIds [] = $ fileId ;
103- }
104- } else {
105- // Flat structure
106- if (!in_array ($ fileId , $ flatFileIds )) {
107- $ flatFileIds [] = $ fileId ;
108- }
109- }
110- $ foundPreview = true ;
111- }
112-
113- foreach ($ fileIds as $ fileId ) {
114- $ this ->processPreviews ($ fileId , flatPath: false );
115- }
116-
117- foreach ($ flatFileIds as $ fileId ) {
118- $ this ->processPreviews ($ fileId , flatPath: true );
119- }
120- return $ foundPreview ;
121- }
122-
12347 /**
12448 * @param array<string|int, string[]> $previewFolders
49+ * @return Preview[]
12550 */
126- private function processPreviews (int $ fileId , bool $ flatPath ): void {
51+ public function migrateFileId (int $ fileId , bool $ flatPath ): array {
52+ $ previews = [];
12753 $ internalPath = $ this ->getInternalFolder ((string )$ fileId , $ flatPath );
128- $ folder = $ this ->appData ->getFolder ($ internalPath );
54+ try {
55+ $ folder = $ this ->appData ->getFolder ($ internalPath );
56+ } catch (NotFoundException ) {
57+ return [];
58+ }
12959
13060 /**
13161 * @var list<array{file: SimpleFile, preview: Preview}> $previewFiles
@@ -152,6 +82,10 @@ private function processPreviews(int $fileId, bool $flatPath): void {
15282 ];
15383 }
15484
85+ if (empty ($ previewFiles )) {
86+ return $ previews ;
87+ }
88+
15589 $ qb = $ this ->connection ->getQueryBuilder ();
15690 $ qb ->select ('storage ' , 'etag ' , 'mimetype ' )
15791 ->from ('filecache ' )
@@ -194,6 +128,8 @@ private function processPreviews(int $fileId, bool $flatPath): void {
194128 $ this ->previewMapper ->delete ($ preview );
195129 throw $ e ;
196130 }
131+
132+ $ previews [] = $ preview ;
197133 }
198134 } else {
199135 // No matching fileId, delete preview
@@ -211,9 +147,11 @@ private function processPreviews(int $fileId, bool $flatPath): void {
211147 }
212148
213149 $ this ->deleteFolder ($ internalPath );
150+
151+ return $ previews ;
214152 }
215153
216- public static function getInternalFolder (string $ name , bool $ flatPath ): string {
154+ private static function getInternalFolder (string $ name , bool $ flatPath ): string {
217155 if ($ flatPath ) {
218156 return $ name ;
219157 }
0 commit comments