1414use OC \Preview \Db \PreviewMapper ;
1515use OC \Preview \Storage \StorageFactory ;
1616use OCP \DB \Exception ;
17+ use OCP \DB \QueryBuilder \IQueryBuilder ;
1718use OCP \Files \AppData \IAppDataFactory ;
19+ use OCP \Files \Cache \ICacheEntry ;
1820use OCP \Files \IAppData ;
1921use OCP \Files \IMimeTypeDetector ;
2022use OCP \Files \IMimeTypeLoader ;
@@ -44,44 +46,45 @@ public function __construct(
4446 }
4547
4648 /**
47- * @param array<string|int, string[]> $previewFolders
49+ * @param list<ICacheEntry|SimpleFile>|null $entries Preview file entries already fetched by the caller.
4850 * @return Preview[]
4951 */
50- public function migrateFileId (int $ fileId , bool $ flatPath ): array {
52+ public function migrateFileId (int $ fileId , bool $ flatPath, ? array $ entries = null ): array {
5153 $ previews = [];
5254 $ internalPath = $ this ->getInternalFolder ((string )$ fileId , $ flatPath );
53- try {
54- $ folder = $ this ->appData ->getFolder ($ internalPath );
55- } catch (NotFoundException ) {
56- return [];
55+
56+ if ($ entries === null ) {
57+ try {
58+ $ entries = $ this ->appData ->getFolder ($ internalPath )->getDirectoryListing ();
59+ } catch (NotFoundException ) {
60+ return [];
61+ }
5762 }
5863
5964 /**
60- * @var list<array{file: SimpleFile, preview: Preview} > $previewFiles
65+ * @var list<Preview> $previewsToInsert
6166 */
62- $ previewFiles = [];
67+ $ previewsToInsert = [];
6368
64- foreach ($ folder ->getDirectoryListing () as $ previewFile ) {
65- $ path = $ fileId . '/ ' . $ previewFile ->getName ();
66- /** @var SimpleFile $previewFile */
69+ foreach ($ entries as $ entry ) {
70+ $ path = $ fileId . '/ ' . $ entry ->getName ();
6771 $ preview = Preview::fromPath ($ path , $ this ->mimeTypeDetector );
6872 if ($ preview === false ) {
6973 $ this ->logger ->error ('Unable to import old preview at path. ' );
7074 continue ;
7175 }
7276 $ preview ->generateId ();
73- $ preview ->setSize ($ previewFile ->getSize ());
74- $ preview ->setMtime ($ previewFile -> getMtime ());
75- $ preview ->setOldFileId ($ previewFile ->getId ());
77+ $ preview ->setSize ($ entry ->getSize ());
78+ $ preview ->setMtime ($ entry -> getMTime ());
79+ $ preview ->setOldFileId ($ entry ->getId ());
7680 $ preview ->setEncrypted (false );
7781
78- $ previewFiles [] = [
79- 'file ' => $ previewFile ,
80- 'preview ' => $ preview ,
81- ];
82+ $ previewsToInsert [] = $ preview ;
8283 }
8384
84- if (empty ($ previewFiles )) {
85+ if (empty ($ previewsToInsert )) {
86+ $ this ->deleteFolder ($ internalPath );
87+
8588 return $ previews ;
8689 }
8790
@@ -95,51 +98,51 @@ public function migrateFileId(int $fileId, bool $flatPath): array {
9598 $ result = $ result ->fetchAssociative ();
9699
97100 if ($ result !== false ) {
98- foreach ($ previewFiles as $ previewFile ) {
99- /** @var Preview $preview */
100- $ preview = $ previewFile ['preview ' ];
101- /** @var SimpleFile $file */
102- $ file = $ previewFile ['file ' ];
103- $ preview ->setStorageId ($ result ['storage ' ]);
104- $ preview ->setEtag ($ result ['etag ' ]);
105- $ preview ->setSourceMimeType ($ this ->mimeTypeLoader ->getMimetypeById ((int )$ result ['mimetype ' ]));
106- $ preview ->generateId ();
107- try {
108- $ preview = $ this ->previewMapper ->insert ($ preview );
109- } catch (Exception ) {
110- // We already have this preview in the preview table, skip
111- $ qb ->delete ('filecache ' )
112- ->where ($ qb ->expr ()->eq ('fileid ' , $ qb ->createNamedParameter ($ file ->getId ())))
113- ->hintShardKey ('storage ' , $ this ->rootFolder ->getMountPoint ()->getNumericStorageId ())
114- ->executeStatement ();
115- continue ;
116- }
117-
118- try {
119- $ this ->storageFactory ->migratePreview ($ preview , $ file );
120- $ qb = $ this ->connection ->getQueryBuilder ();
121- $ qb ->delete ('filecache ' )
122- ->where ($ qb ->expr ()->eq ('fileid ' , $ qb ->createNamedParameter ($ file ->getId ())))
123- ->hintShardKey ('storage ' , $ this ->rootFolder ->getMountPoint ()->getNumericStorageId ())
124- ->executeStatement ();
125- // Do not call $file->delete() as this will also delete the file from the file system
126- } catch (\Exception $ e ) {
127- $ this ->previewMapper ->delete ($ preview );
128- throw $ e ;
101+ $ oldFileIdsToDelete = [];
102+ try {
103+ foreach ($ previewsToInsert as $ preview ) {
104+ $ preview ->setStorageId ($ result ['storage ' ]);
105+ $ preview ->setEtag ($ result ['etag ' ]);
106+ $ preview ->setSourceMimeType ($ this ->mimeTypeLoader ->getMimetypeById ((int )$ result ['mimetype ' ]));
107+ $ preview ->generateId ();
108+ try {
109+ $ preview = $ this ->previewMapper ->insert ($ preview );
110+ } catch (Exception $ e ) {
111+ if ($ e ->getReason () !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION ) {
112+ throw $ e ;
113+ }
114+
115+ // We already have this preview in the preview table, skip
116+ $ oldFileIdsToDelete [] = $ preview ->getOldFileId ();
117+ continue ;
118+ }
119+
120+ try {
121+ $ this ->storageFactory ->migratePreview ($ preview );
122+ // Do not delete the old file via a Node here, as that would also
123+ // delete it from the file system; only its filecache row is stale.
124+ } catch (\Exception $ e ) {
125+ $ this ->previewMapper ->delete ($ preview );
126+ throw $ e ;
127+ }
128+
129+ $ oldFileIdsToDelete [] = $ preview ->getOldFileId ();
130+ $ previews [] = $ preview ;
129131 }
130-
131- $ previews [] = $ preview ;
132+ } finally {
133+ $ this -> deleteOldFileCacheEntries ( $ oldFileIdsToDelete ) ;
132134 }
133135 } else {
134- // No matching fileId, delete preview
136+ // No matching fileId, delete the orphaned preview files themselves.
135137 try {
138+ $ folder = $ this ->appData ->getFolder ($ internalPath );
136139 $ this ->connection ->beginTransaction ();
137- foreach ($ previewFiles as $ previewFile ) {
138- /** @var SimpleFile $file */
139- $ file = $ previewFile ['file ' ];
140+ foreach ($ folder ->getDirectoryListing () as $ file ) {
140141 $ file ->delete ();
141142 }
142143 $ this ->connection ->commit ();
144+ } catch (NotFoundException ) {
145+ // Folder already gone, nothing to clean up.
143146 } catch (Exception ) {
144147 $ this ->connection ->rollback ();
145148 }
@@ -157,6 +160,23 @@ private static function getInternalFolder(string $name, bool $flatPath): string
157160 return implode ('/ ' , str_split (substr (md5 ($ name ), 0 , 7 ))) . '/ ' . $ name ;
158161 }
159162
163+ /**
164+ * @param list<int> $fileIds
165+ */
166+ private function deleteOldFileCacheEntries (array $ fileIds ): void {
167+ if ($ fileIds === []) {
168+ return ;
169+ }
170+
171+ foreach (array_chunk ($ fileIds , 1000 ) as $ chunk ) {
172+ $ qb = $ this ->connection ->getQueryBuilder ();
173+ $ qb ->delete ('filecache ' )
174+ ->where ($ qb ->expr ()->in ('fileid ' , $ qb ->createNamedParameter ($ chunk , IQueryBuilder::PARAM_INT_ARRAY )))
175+ ->hintShardKey ('storage ' , $ this ->rootFolder ->getMountPoint ()->getNumericStorageId ())
176+ ->executeStatement ();
177+ }
178+ }
179+
160180 private function deleteFolder (string $ path ): void {
161181 $ current = $ path ;
162182
@@ -177,10 +197,38 @@ private function deleteFolder(string $path): void {
177197 break ;
178198 }
179199
180- $ folder = $ this ->appData ->getFolder ($ current );
181- if (count ($ folder ->getDirectoryListing ()) !== 0 ) {
200+ if ($ this ->folderHasChildren ($ rootFolderId , $ this ->previewRootPath . $ current )) {
182201 break ;
183202 }
184203 }
185204 }
205+
206+ private function folderHasChildren (int $ storageId , string $ path ): bool {
207+ $ qb = $ this ->connection ->getQueryBuilder ();
208+ $ qb ->select ('fileid ' )
209+ ->from ('filecache ' )
210+ ->where ($ qb ->expr ()->eq ('path_hash ' , $ qb ->createNamedParameter (md5 ($ path ))))
211+ ->andWhere ($ qb ->expr ()->eq ('storage ' , $ qb ->createNamedParameter ($ storageId )))
212+ ->setMaxResults (1 );
213+ $ cursor = $ qb ->executeQuery ();
214+ $ folderId = $ cursor ->fetchOne ();
215+ $ cursor ->closeCursor ();
216+
217+ if ($ folderId === false ) {
218+ // The folder itself is already gone, nothing to check.
219+ return false ;
220+ }
221+
222+ $ qb = $ this ->connection ->getQueryBuilder ();
223+ $ qb ->select ('fileid ' )
224+ ->from ('filecache ' )
225+ ->where ($ qb ->expr ()->eq ('parent ' , $ qb ->createNamedParameter ((int )$ folderId )))
226+ ->andWhere ($ qb ->expr ()->eq ('storage ' , $ qb ->createNamedParameter ($ storageId )))
227+ ->setMaxResults (1 );
228+ $ cursor = $ qb ->executeQuery ();
229+ $ hasChild = $ cursor ->fetchOne () !== false ;
230+ $ cursor ->closeCursor ();
231+
232+ return $ hasChild ;
233+ }
186234}
0 commit comments