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,43 @@ 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 )) {
8586 $ this ->deleteFolder ($ internalPath );
8687
8788 return $ previews ;
@@ -98,56 +99,51 @@ public function migrateFileId(int $fileId, bool $flatPath): array {
9899 $ cursor ->closeCursor ();
99100
100101 if ($ result !== false ) {
101- foreach ($ previewFiles as $ previewFile ) {
102- /** @var Preview $preview */
103- $ preview = $ previewFile ['preview ' ];
104- /** @var SimpleFile $file */
105- $ file = $ previewFile ['file ' ];
106- $ preview ->setStorageId ($ result ['storage ' ]);
107- $ preview ->setEtag ($ result ['etag ' ]);
108- $ preview ->setSourceMimeType ($ this ->mimeTypeLoader ->getMimetypeById ((int )$ result ['mimetype ' ]));
109- $ preview ->generateId ();
110- try {
111- $ preview = $ this ->previewMapper ->insert ($ preview );
112- } catch (Exception $ e ) {
113- if ($ e ->getReason () !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION ) {
114- throw $ e ;
102+ $ oldFileIdsToDelete = [];
103+ try {
104+ foreach ($ previewsToInsert as $ preview ) {
105+ $ preview ->setStorageId ($ result ['storage ' ]);
106+ $ preview ->setEtag ($ result ['etag ' ]);
107+ $ preview ->setSourceMimeType ($ this ->mimeTypeLoader ->getMimetypeById ((int )$ result ['mimetype ' ]));
108+ $ preview ->generateId ();
109+ try {
110+ $ preview = $ this ->previewMapper ->insert ($ preview );
111+ } catch (Exception $ e ) {
112+ if ($ e ->getReason () !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION ) {
113+ throw $ e ;
114+ }
115+
116+ // We already have this preview in the preview table, skip
117+ $ oldFileIdsToDelete [] = $ preview ->getOldFileId ();
118+ continue ;
115119 }
116120
117- $ delete = $ this -> connection -> getQueryBuilder ();
118- // We already have this preview in the preview table, skip
119- $ delete-> delete ( ' filecache ' )
120- -> where ( $ delete -> expr ()-> eq ( ' fileid ' , $ delete-> createNamedParameter ( $ file-> getId ())))
121- -> hintShardKey ( ' storage ' , $ this -> rootFolder -> getMountPoint ()-> getNumericStorageId ())
122- -> executeStatement ( );
123- continue ;
124- }
121+ try {
122+ $ this -> storageFactory -> migratePreview ( $ preview);
123+ // Do not delete the old file via a Node here, as that would also
124+ // delete it from the file system; only its filecache row is stale.
125+ } catch ( \ Exception $ e ) {
126+ $ this -> previewMapper -> delete ( $ preview );
127+ throw $ e ;
128+ }
125129
126- try {
127- $ this ->storageFactory ->migratePreview ($ preview , $ file );
128- $ qb = $ this ->connection ->getQueryBuilder ();
129- $ qb ->delete ('filecache ' )
130- ->where ($ qb ->expr ()->eq ('fileid ' , $ qb ->createNamedParameter ($ file ->getId ())))
131- ->hintShardKey ('storage ' , $ this ->rootFolder ->getMountPoint ()->getNumericStorageId ())
132- ->executeStatement ();
133- // Do not call $file->delete() as this will also delete the file from the file system
134- } catch (\Exception $ e ) {
135- $ this ->previewMapper ->delete ($ preview );
136- throw $ e ;
130+ $ oldFileIdsToDelete [] = $ preview ->getOldFileId ();
131+ $ previews [] = $ preview ;
137132 }
138-
139- $ previews [] = $ preview ;
133+ } finally {
134+ $ this -> deleteOldFileCacheEntries ( $ oldFileIdsToDelete ) ;
140135 }
141136 } else {
142- // No matching fileId, delete preview
137+ // No matching fileId, delete the orphaned preview files themselves.
143138 try {
139+ $ folder = $ this ->appData ->getFolder ($ internalPath );
144140 $ this ->connection ->beginTransaction ();
145- foreach ($ previewFiles as $ previewFile ) {
146- /** @var SimpleFile $file */
147- $ file = $ previewFile ['file ' ];
141+ foreach ($ folder ->getDirectoryListing () as $ file ) {
148142 $ file ->delete ();
149143 }
150144 $ this ->connection ->commit ();
145+ } catch (NotFoundException ) {
146+ // Folder already gone, nothing to clean up.
151147 } catch (Exception ) {
152148 $ this ->connection ->rollback ();
153149 }
@@ -165,6 +161,23 @@ private static function getInternalFolder(string $name, bool $flatPath): string
165161 return implode ('/ ' , str_split (substr (md5 ($ name ), 0 , 7 ))) . '/ ' . $ name ;
166162 }
167163
164+ /**
165+ * @param list<int> $fileIds
166+ */
167+ private function deleteOldFileCacheEntries (array $ fileIds ): void {
168+ if ($ fileIds === []) {
169+ return ;
170+ }
171+
172+ foreach (array_chunk ($ fileIds , 1000 ) as $ chunk ) {
173+ $ qb = $ this ->connection ->getQueryBuilder ();
174+ $ qb ->delete ('filecache ' )
175+ ->where ($ qb ->expr ()->in ('fileid ' , $ qb ->createNamedParameter ($ chunk , IQueryBuilder::PARAM_INT_ARRAY )))
176+ ->hintShardKey ('storage ' , $ this ->rootFolder ->getMountPoint ()->getNumericStorageId ())
177+ ->executeStatement ();
178+ }
179+ }
180+
168181 private function deleteFolder (string $ path ): void {
169182 $ current = $ path ;
170183
@@ -185,14 +198,38 @@ private function deleteFolder(string $path): void {
185198 break ;
186199 }
187200
188- try {
189- $ folder = $ this ->appData ->getFolder ($ current );
190- } catch (NotFoundException ) {
191- break ;
192- }
193- if (count ($ folder ->getDirectoryListing ()) !== 0 ) {
201+ if ($ this ->folderHasChildren ($ rootFolderId , $ this ->previewRootPath . $ current )) {
194202 break ;
195203 }
196204 }
197205 }
206+
207+ private function folderHasChildren (int $ storageId , string $ path ): bool {
208+ $ qb = $ this ->connection ->getQueryBuilder ();
209+ $ qb ->select ('fileid ' )
210+ ->from ('filecache ' )
211+ ->where ($ qb ->expr ()->eq ('path_hash ' , $ qb ->createNamedParameter (md5 ($ path ))))
212+ ->andWhere ($ qb ->expr ()->eq ('storage ' , $ qb ->createNamedParameter ($ storageId )))
213+ ->setMaxResults (1 );
214+ $ cursor = $ qb ->executeQuery ();
215+ $ folderId = $ cursor ->fetchOne ();
216+ $ cursor ->closeCursor ();
217+
218+ if ($ folderId === false ) {
219+ // The folder itself is already gone, nothing to check.
220+ return false ;
221+ }
222+
223+ $ qb = $ this ->connection ->getQueryBuilder ();
224+ $ qb ->select ('fileid ' )
225+ ->from ('filecache ' )
226+ ->where ($ qb ->expr ()->eq ('parent ' , $ qb ->createNamedParameter ((int )$ folderId )))
227+ ->andWhere ($ qb ->expr ()->eq ('storage ' , $ qb ->createNamedParameter ($ storageId )))
228+ ->setMaxResults (1 );
229+ $ cursor = $ qb ->executeQuery ();
230+ $ hasChild = $ cursor ->fetchOne () !== false ;
231+ $ cursor ->closeCursor ();
232+
233+ return $ hasChild ;
234+ }
198235}
0 commit comments