88 */
99namespace OC \Preview ;
1010
11- use OC \Preview \Storage \Root ;
11+ use OC \Preview \Db \PreviewMapper ;
12+ use OC \Preview \Storage \StorageFactory ;
1213use OCP \AppFramework \Utility \ITimeFactory ;
1314use OCP \BackgroundJob \TimedJob ;
1415use OCP \DB \QueryBuilder \IQueryBuilder ;
15- use OCP \Files \IMimeTypeLoader ;
16- use OCP \Files \NotFoundException ;
17- use OCP \Files \NotPermittedException ;
1816use OCP \IDBConnection ;
1917
18+ /**
19+ * @psalm-type FileId int
20+ * @psalm-type StorageId int
21+ */
2022class BackgroundCleanupJob extends TimedJob {
2123
2224 public function __construct (
2325 ITimeFactory $ timeFactory ,
24- private IDBConnection $ connection ,
25- private Root $ previewFolder ,
26- private IMimeTypeLoader $ mimeTypeLoader ,
27- private bool $ isCLI ,
26+ readonly private IDBConnection $ connection ,
27+ readonly private PreviewMapper $ previewMapper ,
28+ readonly private StorageFactory $ storageFactory ,
29+ readonly private bool $ isCLI ,
2830 ) {
2931 parent ::__construct ($ timeFactory );
3032 // Run at most once an hour
3133 $ this ->setInterval (60 * 60 );
3234 $ this ->setTimeSensitivity (self ::TIME_INSENSITIVE );
3335 }
3436
35- public function run ($ argument ) {
36- foreach ($ this ->getDeletedFiles () as $ fileId ) {
37- try {
38- $ preview = $ this ->previewFolder ->getFolder ((string )$ fileId );
39- $ preview ->delete ();
40- } catch (NotFoundException $ e ) {
41- // continue
42- } catch (NotPermittedException $ e ) {
43- // continue
37+ public function run ($ argument ): void {
38+ foreach ($ this ->getDeletedFiles () as $ chunk ) {
39+ foreach ($ chunk as $ storage => $ fileIds ) {
40+ foreach ($ this ->previewMapper ->getByFileIds ($ storage , $ fileIds ) as $ previews ) {
41+ $ previewIds = [];
42+ foreach ($ previews as $ preview ) {
43+ $ previewIds [] = $ preview ->getId ();
44+ $ this ->storageFactory ->deletePreview ($ preview );
45+ }
46+
47+ $ this ->previewMapper ->deleteByIds ($ storage , $ previewIds );
48+ };
4449 }
4550 }
4651 }
4752
53+ /**
54+ * @return \Iterator<array<StorageId, FileId[]>>
55+ */
4856 private function getDeletedFiles (): \Iterator {
49- yield from $ this ->getOldPreviewLocations ();
50- yield from $ this ->getNewPreviewLocations ();
51- }
52-
53- private function getOldPreviewLocations (): \Iterator {
54- if ($ this ->connection ->getShardDefinition ('filecache ' )) {
55- // sharding is new enough that we don't need to support this
56- return ;
57- }
58-
59- $ qb = $ this ->connection ->getQueryBuilder ();
60- $ qb ->select ('a.name ' )
61- ->from ('filecache ' , 'a ' )
62- ->leftJoin ('a ' , 'filecache ' , 'b ' , $ qb ->expr ()->eq (
63- $ qb ->expr ()->castColumn ('a.name ' , IQueryBuilder::PARAM_INT ), 'b.fileid '
64- ))
65- ->where (
66- $ qb ->expr ()->andX (
67- $ qb ->expr ()->isNull ('b.fileid ' ),
68- $ qb ->expr ()->eq ('a.storage ' , $ qb ->createNamedParameter ($ this ->previewFolder ->getStorageId ())),
69- $ qb ->expr ()->eq ('a.parent ' , $ qb ->createNamedParameter ($ this ->previewFolder ->getId ())),
70- $ qb ->expr ()->like ('a.name ' , $ qb ->createNamedParameter ('__% ' )),
71- $ qb ->expr ()->eq ('a.mimetype ' , $ qb ->createNamedParameter ($ this ->mimeTypeLoader ->getId ('httpd/unix-directory ' )))
72- )
73- );
74-
75- if (!$ this ->isCLI ) {
76- $ qb ->setMaxResults (10 );
77- }
78-
79- $ cursor = $ qb ->executeQuery ();
80-
81- while ($ row = $ cursor ->fetch ()) {
82- yield $ row ['name ' ];
83- }
84-
85- $ cursor ->closeCursor ();
86- }
87-
88- private function getNewPreviewLocations (): \Iterator {
89- $ qb = $ this ->connection ->getQueryBuilder ();
90- $ qb ->select ('path ' , 'mimetype ' )
91- ->from ('filecache ' )
92- ->where ($ qb ->expr ()->eq ('fileid ' , $ qb ->createNamedParameter ($ this ->previewFolder ->getId ())));
93- $ cursor = $ qb ->executeQuery ();
94- $ data = $ cursor ->fetch ();
95- $ cursor ->closeCursor ();
96-
97- if ($ data === null ) {
98- return [];
99- }
100-
10157 if ($ this ->connection ->getShardDefinition ('filecache ' )) {
102- $ chunks = $ this ->getAllPreviewIds ($ data [ ' path ' ], 1000 );
58+ $ chunks = $ this ->getAllPreviewIds (1000 );
10359 foreach ($ chunks as $ chunk ) {
104- yield from $ this ->findMissingSources ($ chunk );
60+ foreach ($ chunk as $ storage => $ preview ) {
61+ yield [$ storage => $ this ->findMissingSources ($ storage , $ preview )];
62+ }
10563 }
10664
10765 return ;
10866 }
10967
110- /*
111- * This lovely like is the result of the way the new previews are stored
112- * We take the md5 of the name (fileid) and split the first 7 chars. That way
113- * there are not a gazillion files in the root of the preview appdata.
114- */
115- $ like = $ this ->connection ->escapeLikeParameter ($ data ['path ' ]) . '/_/_/_/_/_/_/_/% ' ;
116-
11768 /*
11869 * Deleting a file will not delete related previews right away.
11970 *
@@ -130,71 +81,85 @@ private function getNewPreviewLocations(): \Iterator {
13081 * If the related file is deleted, b.fileid will be null and the preview folder can be deleted.
13182 */
13283 $ qb = $ this ->connection ->getQueryBuilder ();
133- $ qb ->select ('a.name ' )
134- ->from ('filecache ' , 'a ' )
135- ->leftJoin ('a ' , 'filecache ' , 'b ' , $ qb ->expr ()->eq (
136- $ qb -> expr ()-> castColumn ( ' a.name ' , IQueryBuilder:: PARAM_INT ), ' b .fileid '
84+ $ qb ->select ('p.storage_id ' , ' p.file_id ' )
85+ ->from ('previews ' , 'p ' )
86+ ->leftJoin ('p ' , 'filecache ' , 'f ' , $ qb ->expr ()->eq (
87+ ' p.file_id ' , ' f .fileid '
13788 ))
138- ->where (
139- $ qb ->expr ()->andX (
140- $ qb ->expr ()->eq ('a.storage ' , $ qb ->createNamedParameter ($ this ->previewFolder ->getStorageId ())),
141- $ qb ->expr ()->isNull ('b.fileid ' ),
142- $ qb ->expr ()->like ('a.path ' , $ qb ->createNamedParameter ($ like )),
143- $ qb ->expr ()->eq ('a.mimetype ' , $ qb ->createNamedParameter ($ this ->mimeTypeLoader ->getId ('httpd/unix-directory ' )))
144- )
145- );
89+ ->where ($ qb ->expr ()->isNull ('f.fileid ' ));
14690
14791 if (!$ this ->isCLI ) {
14892 $ qb ->setMaxResults (10 );
14993 }
15094
15195 $ cursor = $ qb ->executeQuery ();
15296
97+ $ lastStorageId = null ;
98+ /** @var FileId[] $tmpResult */
99+ $ tmpResult = [];
153100 while ($ row = $ cursor ->fetch ()) {
154- yield $ row ['name ' ];
101+ if ($ lastStorageId === null ) {
102+ $ lastStorageId = $ row ['storage_id ' ];
103+ } else if ($ lastStorageId !== $ row ['storage_id ' ]) {
104+ yield [$ lastStorageId => $ tmpResult ];
105+ $ tmpResult = [];
106+ $ lastStorageId = $ row ['storage_id ' ];
107+ }
108+ $ tmpResult [] = $ row ['file_id ' ];
109+ }
110+
111+ if (!empty ($ tmpResult )) {
112+ yield [$ lastStorageId => $ tmpResult ];
155113 }
156114
157115 $ cursor ->closeCursor ();
158116 }
159117
160- private function getAllPreviewIds ( string $ previewRoot , int $ chunkSize ): \ Iterator {
161- // See `getNewPreviewLocations` for some more info about the logic here
162- $ like = $ this -> connection -> escapeLikeParameter ( $ previewRoot ) . ' /_/_/_/_/_/_/_/% ' ;
163-
118+ /**
119+ * @return \Iterator<array<StorageId, FileId[]>>
120+ */
121+ private function getAllPreviewIds ( int $ chunkSize ): \ Iterator {
164122 $ qb = $ this ->connection ->getQueryBuilder ();
165- $ qb ->select ('name ' , 'fileid ' )
166- ->from ('filecache ' )
123+ $ qb ->select ('id ' , 'file_id ' , ' storage_id ' )
124+ ->from ('previews ' )
167125 ->where (
168- $ qb ->expr ()->andX (
169- $ qb ->expr ()->eq ('storage ' , $ qb ->createNamedParameter ($ this ->previewFolder ->getStorageId ())),
170- $ qb ->expr ()->like ('path ' , $ qb ->createNamedParameter ($ like )),
171- $ qb ->expr ()->eq ('mimetype ' , $ qb ->createNamedParameter ($ this ->mimeTypeLoader ->getId ('httpd/unix-directory ' ))),
172- $ qb ->expr ()->gt ('fileid ' , $ qb ->createParameter ('min_id ' )),
173- )
126+ $ qb ->expr ()->gt ('id ' , $ qb ->createParameter ('min_id ' )),
174127 )
175- ->orderBy ('fileid ' , 'ASC ' )
128+ ->orderBy ('id ' , 'ASC ' )
176129 ->setMaxResults ($ chunkSize );
177130
178131 $ minId = 0 ;
179132 while (true ) {
180133 $ qb ->setParameter ('min_id ' , $ minId );
181134 $ rows = $ qb ->executeQuery ()->fetchAll ();
182135 if (count ($ rows ) > 0 ) {
183- $ minId = $ rows [count ($ rows ) - 1 ]['fileid ' ];
184- yield array_map (function ($ row ) {
185- return (int )$ row ['name ' ];
186- }, $ rows );
136+ $ minId = $ rows [count ($ rows ) - 1 ]['id ' ];
137+ $ result = [];
138+ foreach ($ rows as $ row ) {
139+ if (!isset ($ result [$ row ['storage_id ' ]])) {
140+ $ result [$ row ['storage_id ' ]] = [];
141+ }
142+ $ result [$ row ['storage_id ' ]][] = $ row ['file_id ' ];
143+ }
144+ yield $ result ;
187145 } else {
188146 break ;
189147 }
190148 }
191149 }
192150
193- private function findMissingSources (array $ ids ): array {
151+ /**
152+ * @param FileId[] $ids
153+ * @return FileId[]
154+ */
155+ private function findMissingSources (int $ storage , array $ ids ): array {
194156 $ qb = $ this ->connection ->getQueryBuilder ();
195157 $ qb ->select ('fileid ' )
196158 ->from ('filecache ' )
197- ->where ($ qb ->expr ()->in ('fileid ' , $ qb ->createNamedParameter ($ ids , IQueryBuilder::PARAM_INT_ARRAY )));
159+ ->where ($ qb ->expr ()->andX (
160+ $ qb ->expr ()->in ('fileid ' , $ qb ->createNamedParameter ($ ids , IQueryBuilder::PARAM_INT_ARRAY )),
161+ $ qb ->expr ()->eq ('storage ' , $ qb ->createNamedParameter ($ storage , IQueryBuilder::PARAM_INT )),
162+ ));
198163 $ found = $ qb ->executeQuery ()->fetchAll (\PDO ::FETCH_COLUMN );
199164 return array_diff ($ ids , $ found );
200165 }
0 commit comments