11<?php
2+
3+ declare (strict_types=1 );
4+
25/**
36 * @copyright Copyright (c) 2016, ownCloud, Inc.
47 *
2427 */
2528namespace OCA \Files_Sharing ;
2629
30+ use OCP \AppFramework \Db \TTransactional ;
2731use OCP \AppFramework \Utility \ITimeFactory ;
2832use OCP \BackgroundJob \TimedJob ;
33+ use OCP \DB \QueryBuilder \IQueryBuilder ;
34+ use OCP \IDBConnection ;
35+ use PDO ;
36+ use Psr \Log \LoggerInterface ;
37+ use function array_map ;
2938
3039/**
3140 * Delete all share entries that have no matching entries in the file cache table.
3241 */
3342class DeleteOrphanedSharesJob extends TimedJob {
43+
44+ use TTransactional;
45+
46+ private const CHUNK_SIZE = 1000 ;
47+
48+ private const INTERVAL = 24 * 60 * 60 ; // 1 day
49+
50+ private IDBConnection $ db ;
51+
52+ private LoggerInterface $ logger ;
53+
3454 /**
3555 * sets the correct interval for this timed job
3656 */
37- public function __construct (ITimeFactory $ time ) {
57+ public function __construct (
58+ ITimeFactory $ time ,
59+ IDBConnection $ db ,
60+ LoggerInterface $ logger
61+ ) {
3862 parent ::__construct ($ time );
3963
40- $ this ->setInterval (24 * 60 * 60 ); // 1 day
64+ $ this ->db = $ db ;
65+
66+ $ this ->setInterval (self ::INTERVAL ); // 1 day
4167 $ this ->setTimeSensitivity (self ::TIME_INSENSITIVE );
68+ $ this ->logger = $ logger ;
4269 }
4370
4471 /**
@@ -47,15 +74,45 @@ public function __construct(ITimeFactory $time) {
4774 * @param array $argument unused argument
4875 */
4976 public function run ($ argument ) {
50- $ connection = \OC ::$ server ->getDatabaseConnection ();
51- $ logger = \OC ::$ server ->getLogger ();
52-
53- $ sql =
54- 'DELETE FROM `*PREFIX*share` ' .
55- 'WHERE `item_type` in ( \'file \', \'folder \') ' .
56- 'AND NOT EXISTS (SELECT `fileid` FROM `*PREFIX*filecache` WHERE `file_source` = `fileid`) ' ;
77+ $ qbSelect = $ this ->db ->getQueryBuilder ();
78+ $ qbSelect ->select ('id ' )
79+ ->from ('share ' , 's ' )
80+ ->leftJoin ('s ' , 'filecache ' , 'fc ' , $ qbSelect ->expr ()->eq ('s.file_source ' , 'fc.fileid ' ))
81+ ->where ($ qbSelect ->expr ()->isNull ('fc.fileid ' ))
82+ ->setMaxResults (self ::CHUNK_SIZE );
83+ $ deleteQb = $ this ->db ->getQueryBuilder ();
84+ $ deleteQb ->delete ('share ' )
85+ ->where (
86+ $ deleteQb ->expr ()->in ('id ' , $ deleteQb ->createParameter ('ids ' ), IQueryBuilder::PARAM_INT_ARRAY )
87+ );
5788
58- $ deletedEntries = $ connection ->executeUpdate ($ sql );
59- $ logger ->debug ("$ deletedEntries orphaned share(s) deleted " , ['app ' => 'DeleteOrphanedSharesJob ' ]);
89+ /**
90+ * Read a chunk of orphan rows and delete them. Continue as long as the
91+ * chunk is filled and time before the next cron run does not run out.
92+ *
93+ * Note: With isolation level READ COMMITTED, the database will allow
94+ * other transactions to delete rows between our SELECT and DELETE. In
95+ * that (unlikely) case, our DELETE will have fewer affected rows than
96+ * IDs passed for the WHERE IN. If this happens while processing a full
97+ * chunk, the logic below will stop prematurely.
98+ * Note: The queries below are optimized for low database locking. They
99+ * could be combined into one single DELETE with join or sub query, but
100+ * that has shown to (dead)lock often.
101+ */
102+ $ cutOff = $ this ->time ->getTime () + self ::INTERVAL ;
103+ do {
104+ $ deleted = $ this ->atomic (function () use ($ qbSelect , $ deleteQb ) {
105+ $ result = $ qbSelect ->executeQuery ();
106+ $ ids = array_map ('intval ' , $ result ->fetchAll (PDO ::FETCH_COLUMN ));
107+ $ result ->closeCursor ();
108+ $ deleteQb ->setParameter ('ids ' , $ ids , IQueryBuilder::PARAM_INT_ARRAY );
109+ $ deleted = $ deleteQb ->executeStatement ();
110+ $ this ->logger ->debug ("{deleted} orphaned share(s) deleted " , [
111+ 'app ' => 'DeleteOrphanedSharesJob ' ,
112+ 'deleted ' => $ deleted ,
113+ ]);
114+ return $ deleted ;
115+ }, $ this ->db );
116+ } while ($ deleted >= self ::CHUNK_SIZE && $ this ->time ->getTime () <= $ cutOff );
60117 }
61118}
0 commit comments