media_repository.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741
  1. # Copyright 2014-2016 OpenMarket Ltd
  2. # Copyright 2020-2021 The Matrix.org Foundation C.I.C.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. from enum import Enum
  16. from typing import (
  17. TYPE_CHECKING,
  18. Any,
  19. Collection,
  20. Dict,
  21. Iterable,
  22. List,
  23. Optional,
  24. Tuple,
  25. Union,
  26. cast,
  27. )
  28. from synapse.api.constants import Direction
  29. from synapse.logging.opentracing import trace
  30. from synapse.storage._base import SQLBaseStore
  31. from synapse.storage.database import (
  32. DatabasePool,
  33. LoggingDatabaseConnection,
  34. LoggingTransaction,
  35. )
  36. from synapse.types import JsonDict, UserID
  37. if TYPE_CHECKING:
  38. from synapse.server import HomeServer
  39. BG_UPDATE_REMOVE_MEDIA_REPO_INDEX_WITHOUT_METHOD_2 = (
  40. "media_repository_drop_index_wo_method_2"
  41. )
  42. class MediaSortOrder(Enum):
  43. """
  44. Enum to define the sorting method used when returning media with
  45. get_local_media_by_user_paginate
  46. """
  47. MEDIA_ID = "media_id"
  48. UPLOAD_NAME = "upload_name"
  49. CREATED_TS = "created_ts"
  50. LAST_ACCESS_TS = "last_access_ts"
  51. MEDIA_LENGTH = "media_length"
  52. MEDIA_TYPE = "media_type"
  53. QUARANTINED_BY = "quarantined_by"
  54. SAFE_FROM_QUARANTINE = "safe_from_quarantine"
  55. class MediaRepositoryBackgroundUpdateStore(SQLBaseStore):
  56. def __init__(
  57. self,
  58. database: DatabasePool,
  59. db_conn: LoggingDatabaseConnection,
  60. hs: "HomeServer",
  61. ):
  62. super().__init__(database, db_conn, hs)
  63. self.db_pool.updates.register_background_index_update(
  64. update_name="local_media_repository_url_idx",
  65. index_name="local_media_repository_url_idx",
  66. table="local_media_repository",
  67. columns=["created_ts"],
  68. where_clause="url_cache IS NOT NULL",
  69. )
  70. # The following the updates add the method to the unique constraint of
  71. # the thumbnail databases. That fixes an issue, where thumbnails of the
  72. # same resolution, but different methods could overwrite one another.
  73. # This can happen with custom thumbnail configs or with dynamic thumbnailing.
  74. self.db_pool.updates.register_background_index_update(
  75. update_name="local_media_repository_thumbnails_method_idx",
  76. index_name="local_media_repository_thumbn_media_id_width_height_method_key",
  77. table="local_media_repository_thumbnails",
  78. columns=[
  79. "media_id",
  80. "thumbnail_width",
  81. "thumbnail_height",
  82. "thumbnail_type",
  83. "thumbnail_method",
  84. ],
  85. unique=True,
  86. )
  87. self.db_pool.updates.register_background_index_update(
  88. update_name="remote_media_repository_thumbnails_method_idx",
  89. index_name="remote_media_repository_thumbn_media_origin_id_width_height_method_key",
  90. table="remote_media_cache_thumbnails",
  91. columns=[
  92. "media_origin",
  93. "media_id",
  94. "thumbnail_width",
  95. "thumbnail_height",
  96. "thumbnail_type",
  97. "thumbnail_method",
  98. ],
  99. unique=True,
  100. )
  101. self.db_pool.updates.register_background_update_handler(
  102. BG_UPDATE_REMOVE_MEDIA_REPO_INDEX_WITHOUT_METHOD_2,
  103. self._drop_media_index_without_method,
  104. )
  105. async def _drop_media_index_without_method(
  106. self, progress: JsonDict, batch_size: int
  107. ) -> int:
  108. """background update handler which removes the old constraints.
  109. Note that this is only run on postgres.
  110. """
  111. def f(txn: LoggingTransaction) -> None:
  112. txn.execute(
  113. "ALTER TABLE local_media_repository_thumbnails DROP CONSTRAINT IF EXISTS local_media_repository_thumbn_media_id_thumbnail_width_thum_key"
  114. )
  115. txn.execute(
  116. "ALTER TABLE remote_media_cache_thumbnails DROP CONSTRAINT IF EXISTS remote_media_cache_thumbnails_media_origin_media_id_thumbna_key"
  117. )
  118. await self.db_pool.runInteraction("drop_media_indices_without_method", f)
  119. await self.db_pool.updates._end_background_update(
  120. BG_UPDATE_REMOVE_MEDIA_REPO_INDEX_WITHOUT_METHOD_2
  121. )
  122. return 1
  123. class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore):
  124. """Persistence for attachments and avatars"""
  125. def __init__(
  126. self,
  127. database: DatabasePool,
  128. db_conn: LoggingDatabaseConnection,
  129. hs: "HomeServer",
  130. ):
  131. super().__init__(database, db_conn, hs)
  132. self.server_name: str = hs.hostname
  133. async def get_local_media(self, media_id: str) -> Optional[Dict[str, Any]]:
  134. """Get the metadata for a local piece of media
  135. Returns:
  136. None if the media_id doesn't exist.
  137. """
  138. return await self.db_pool.simple_select_one(
  139. "local_media_repository",
  140. {"media_id": media_id},
  141. (
  142. "media_type",
  143. "media_length",
  144. "upload_name",
  145. "created_ts",
  146. "quarantined_by",
  147. "url_cache",
  148. "safe_from_quarantine",
  149. ),
  150. allow_none=True,
  151. desc="get_local_media",
  152. )
  153. async def get_local_media_by_user_paginate(
  154. self,
  155. start: int,
  156. limit: int,
  157. user_id: str,
  158. order_by: str = MediaSortOrder.CREATED_TS.value,
  159. direction: Direction = Direction.FORWARDS,
  160. ) -> Tuple[List[Dict[str, Any]], int]:
  161. """Get a paginated list of metadata for a local piece of media
  162. which an user_id has uploaded
  163. Args:
  164. start: offset in the list
  165. limit: maximum amount of media_ids to retrieve
  166. user_id: fully-qualified user id
  167. order_by: the sort order of the returned list
  168. direction: sort ascending or descending
  169. Returns:
  170. A paginated list of all metadata of user's media,
  171. plus the total count of all the user's media
  172. """
  173. def get_local_media_by_user_paginate_txn(
  174. txn: LoggingTransaction,
  175. ) -> Tuple[List[Dict[str, Any]], int]:
  176. # Set ordering
  177. order_by_column = MediaSortOrder(order_by).value
  178. if direction == Direction.BACKWARDS:
  179. order = "DESC"
  180. else:
  181. order = "ASC"
  182. args: List[Union[str, int]] = [user_id]
  183. sql = """
  184. SELECT COUNT(*) as total_media
  185. FROM local_media_repository
  186. WHERE user_id = ?
  187. """
  188. txn.execute(sql, args)
  189. count = cast(Tuple[int], txn.fetchone())[0]
  190. sql = """
  191. SELECT
  192. "media_id",
  193. "media_type",
  194. "media_length",
  195. "upload_name",
  196. "created_ts",
  197. "last_access_ts",
  198. "quarantined_by",
  199. "safe_from_quarantine"
  200. FROM local_media_repository
  201. WHERE user_id = ?
  202. ORDER BY {order_by_column} {order}, media_id ASC
  203. LIMIT ? OFFSET ?
  204. """.format(
  205. order_by_column=order_by_column,
  206. order=order,
  207. )
  208. args += [limit, start]
  209. txn.execute(sql, args)
  210. media = self.db_pool.cursor_to_dict(txn)
  211. return media, count
  212. return await self.db_pool.runInteraction(
  213. "get_local_media_by_user_paginate_txn", get_local_media_by_user_paginate_txn
  214. )
  215. async def get_local_media_ids(
  216. self,
  217. before_ts: int,
  218. size_gt: int,
  219. keep_profiles: bool,
  220. include_quarantined_media: bool,
  221. include_protected_media: bool,
  222. ) -> List[str]:
  223. """
  224. Retrieve a list of media IDs from the local media store.
  225. Args:
  226. before_ts: Only retrieve IDs from media that was either last accessed
  227. (or if never accessed, created) before the given UNIX timestamp in ms.
  228. size_gt: Only retrieve IDs from media that has a size (in bytes) greater than
  229. the given integer.
  230. keep_profiles: If True, exclude media IDs from the results that are used in the
  231. following situations:
  232. * global profile user avatar
  233. * per-room profile user avatar
  234. * room avatar
  235. * a user's avatar in the user directory
  236. include_quarantined_media: If False, exclude media IDs from the results that have
  237. been marked as quarantined.
  238. include_protected_media: If False, exclude media IDs from the results that have
  239. been marked as protected from quarantine.
  240. Returns:
  241. A list of local media IDs.
  242. """
  243. # to find files that have never been accessed (last_access_ts IS NULL)
  244. # compare with `created_ts`
  245. sql = """
  246. SELECT media_id
  247. FROM local_media_repository AS lmr
  248. WHERE
  249. ( last_access_ts < ?
  250. OR ( created_ts < ? AND last_access_ts IS NULL ) )
  251. AND media_length > ?
  252. """
  253. if keep_profiles:
  254. sql_keep = """
  255. AND (
  256. NOT EXISTS
  257. (SELECT 1
  258. FROM profiles
  259. WHERE profiles.avatar_url = '{media_prefix}' || lmr.media_id)
  260. AND NOT EXISTS
  261. (SELECT 1
  262. FROM room_memberships
  263. WHERE room_memberships.avatar_url = '{media_prefix}' || lmr.media_id)
  264. AND NOT EXISTS
  265. (SELECT 1
  266. FROM user_directory
  267. WHERE user_directory.avatar_url = '{media_prefix}' || lmr.media_id)
  268. AND NOT EXISTS
  269. (SELECT 1
  270. FROM room_stats_state
  271. WHERE room_stats_state.avatar = '{media_prefix}' || lmr.media_id)
  272. )
  273. """.format(
  274. media_prefix="mxc://%s/" % (self.server_name,),
  275. )
  276. sql += sql_keep
  277. if include_quarantined_media is False:
  278. # Do not include media that has been quarantined
  279. sql += """
  280. AND quarantined_by IS NULL
  281. """
  282. if include_protected_media is False:
  283. # Do not include media that has been protected from quarantine
  284. sql += """
  285. AND NOT safe_from_quarantine
  286. """
  287. def _get_local_media_ids_txn(txn: LoggingTransaction) -> List[str]:
  288. txn.execute(sql, (before_ts, before_ts, size_gt))
  289. return [row[0] for row in txn]
  290. return await self.db_pool.runInteraction(
  291. "get_local_media_ids", _get_local_media_ids_txn
  292. )
  293. @trace
  294. async def store_local_media(
  295. self,
  296. media_id: str,
  297. media_type: str,
  298. time_now_ms: int,
  299. upload_name: Optional[str],
  300. media_length: int,
  301. user_id: UserID,
  302. url_cache: Optional[str] = None,
  303. ) -> None:
  304. await self.db_pool.simple_insert(
  305. "local_media_repository",
  306. {
  307. "media_id": media_id,
  308. "media_type": media_type,
  309. "created_ts": time_now_ms,
  310. "upload_name": upload_name,
  311. "media_length": media_length,
  312. "user_id": user_id.to_string(),
  313. "url_cache": url_cache,
  314. },
  315. desc="store_local_media",
  316. )
  317. async def mark_local_media_as_safe(self, media_id: str, safe: bool = True) -> None:
  318. """Mark a local media as safe or unsafe from quarantining."""
  319. await self.db_pool.simple_update_one(
  320. table="local_media_repository",
  321. keyvalues={"media_id": media_id},
  322. updatevalues={"safe_from_quarantine": safe},
  323. desc="mark_local_media_as_safe",
  324. )
  325. async def get_url_cache(self, url: str, ts: int) -> Optional[Dict[str, Any]]:
  326. """Get the media_id and ts for a cached URL as of the given timestamp
  327. Returns:
  328. None if the URL isn't cached.
  329. """
  330. def get_url_cache_txn(txn: LoggingTransaction) -> Optional[Dict[str, Any]]:
  331. # get the most recently cached result (relative to the given ts)
  332. sql = (
  333. "SELECT response_code, etag, expires_ts, og, media_id, download_ts"
  334. " FROM local_media_repository_url_cache"
  335. " WHERE url = ? AND download_ts <= ?"
  336. " ORDER BY download_ts DESC LIMIT 1"
  337. )
  338. txn.execute(sql, (url, ts))
  339. row = txn.fetchone()
  340. if not row:
  341. # ...or if we've requested a timestamp older than the oldest
  342. # copy in the cache, return the oldest copy (if any)
  343. sql = (
  344. "SELECT response_code, etag, expires_ts, og, media_id, download_ts"
  345. " FROM local_media_repository_url_cache"
  346. " WHERE url = ? AND download_ts > ?"
  347. " ORDER BY download_ts ASC LIMIT 1"
  348. )
  349. txn.execute(sql, (url, ts))
  350. row = txn.fetchone()
  351. if not row:
  352. return None
  353. return dict(
  354. zip(
  355. (
  356. "response_code",
  357. "etag",
  358. "expires_ts",
  359. "og",
  360. "media_id",
  361. "download_ts",
  362. ),
  363. row,
  364. )
  365. )
  366. return await self.db_pool.runInteraction("get_url_cache", get_url_cache_txn)
  367. async def store_url_cache(
  368. self,
  369. url: str,
  370. response_code: int,
  371. etag: Optional[str],
  372. expires_ts: int,
  373. og: Optional[str],
  374. media_id: str,
  375. download_ts: int,
  376. ) -> None:
  377. await self.db_pool.simple_insert(
  378. "local_media_repository_url_cache",
  379. {
  380. "url": url,
  381. "response_code": response_code,
  382. "etag": etag,
  383. "expires_ts": expires_ts,
  384. "og": og,
  385. "media_id": media_id,
  386. "download_ts": download_ts,
  387. },
  388. desc="store_url_cache",
  389. )
  390. async def get_local_media_thumbnails(self, media_id: str) -> List[Dict[str, Any]]:
  391. return await self.db_pool.simple_select_list(
  392. "local_media_repository_thumbnails",
  393. {"media_id": media_id},
  394. (
  395. "thumbnail_width",
  396. "thumbnail_height",
  397. "thumbnail_method",
  398. "thumbnail_type",
  399. "thumbnail_length",
  400. ),
  401. desc="get_local_media_thumbnails",
  402. )
  403. @trace
  404. async def store_local_thumbnail(
  405. self,
  406. media_id: str,
  407. thumbnail_width: int,
  408. thumbnail_height: int,
  409. thumbnail_type: str,
  410. thumbnail_method: str,
  411. thumbnail_length: int,
  412. ) -> None:
  413. await self.db_pool.simple_upsert(
  414. table="local_media_repository_thumbnails",
  415. keyvalues={
  416. "media_id": media_id,
  417. "thumbnail_width": thumbnail_width,
  418. "thumbnail_height": thumbnail_height,
  419. "thumbnail_method": thumbnail_method,
  420. "thumbnail_type": thumbnail_type,
  421. },
  422. values={"thumbnail_length": thumbnail_length},
  423. desc="store_local_thumbnail",
  424. )
  425. async def get_cached_remote_media(
  426. self, origin: str, media_id: str
  427. ) -> Optional[Dict[str, Any]]:
  428. return await self.db_pool.simple_select_one(
  429. "remote_media_cache",
  430. {"media_origin": origin, "media_id": media_id},
  431. (
  432. "media_type",
  433. "media_length",
  434. "upload_name",
  435. "created_ts",
  436. "filesystem_id",
  437. "quarantined_by",
  438. ),
  439. allow_none=True,
  440. desc="get_cached_remote_media",
  441. )
  442. async def store_cached_remote_media(
  443. self,
  444. origin: str,
  445. media_id: str,
  446. media_type: str,
  447. media_length: int,
  448. time_now_ms: int,
  449. upload_name: Optional[str],
  450. filesystem_id: str,
  451. ) -> None:
  452. await self.db_pool.simple_insert(
  453. "remote_media_cache",
  454. {
  455. "media_origin": origin,
  456. "media_id": media_id,
  457. "media_type": media_type,
  458. "media_length": media_length,
  459. "created_ts": time_now_ms,
  460. "upload_name": upload_name,
  461. "filesystem_id": filesystem_id,
  462. "last_access_ts": time_now_ms,
  463. },
  464. desc="store_cached_remote_media",
  465. )
  466. async def update_cached_last_access_time(
  467. self,
  468. local_media: Iterable[str],
  469. remote_media: Iterable[Tuple[str, str]],
  470. time_ms: int,
  471. ) -> None:
  472. """Updates the last access time of the given media
  473. Args:
  474. local_media: Set of media_ids
  475. remote_media: Set of (server_name, media_id)
  476. time_ms: Current time in milliseconds
  477. """
  478. def update_cache_txn(txn: LoggingTransaction) -> None:
  479. sql = (
  480. "UPDATE remote_media_cache SET last_access_ts = ?"
  481. " WHERE media_origin = ? AND media_id = ?"
  482. )
  483. txn.execute_batch(
  484. sql,
  485. (
  486. (time_ms, media_origin, media_id)
  487. for media_origin, media_id in remote_media
  488. ),
  489. )
  490. sql = (
  491. "UPDATE local_media_repository SET last_access_ts = ?"
  492. " WHERE media_id = ?"
  493. )
  494. txn.execute_batch(sql, ((time_ms, media_id) for media_id in local_media))
  495. await self.db_pool.runInteraction(
  496. "update_cached_last_access_time", update_cache_txn
  497. )
  498. async def get_remote_media_thumbnails(
  499. self, origin: str, media_id: str
  500. ) -> List[Dict[str, Any]]:
  501. return await self.db_pool.simple_select_list(
  502. "remote_media_cache_thumbnails",
  503. {"media_origin": origin, "media_id": media_id},
  504. (
  505. "thumbnail_width",
  506. "thumbnail_height",
  507. "thumbnail_method",
  508. "thumbnail_type",
  509. "thumbnail_length",
  510. "filesystem_id",
  511. ),
  512. desc="get_remote_media_thumbnails",
  513. )
  514. @trace
  515. async def get_remote_media_thumbnail(
  516. self,
  517. origin: str,
  518. media_id: str,
  519. t_width: int,
  520. t_height: int,
  521. t_type: str,
  522. ) -> Optional[Dict[str, Any]]:
  523. """Fetch the thumbnail info of given width, height and type."""
  524. return await self.db_pool.simple_select_one(
  525. table="remote_media_cache_thumbnails",
  526. keyvalues={
  527. "media_origin": origin,
  528. "media_id": media_id,
  529. "thumbnail_width": t_width,
  530. "thumbnail_height": t_height,
  531. "thumbnail_type": t_type,
  532. },
  533. retcols=(
  534. "thumbnail_width",
  535. "thumbnail_height",
  536. "thumbnail_method",
  537. "thumbnail_type",
  538. "thumbnail_length",
  539. "filesystem_id",
  540. ),
  541. allow_none=True,
  542. desc="get_remote_media_thumbnail",
  543. )
  544. @trace
  545. async def store_remote_media_thumbnail(
  546. self,
  547. origin: str,
  548. media_id: str,
  549. filesystem_id: str,
  550. thumbnail_width: int,
  551. thumbnail_height: int,
  552. thumbnail_type: str,
  553. thumbnail_method: str,
  554. thumbnail_length: int,
  555. ) -> None:
  556. await self.db_pool.simple_upsert(
  557. table="remote_media_cache_thumbnails",
  558. keyvalues={
  559. "media_origin": origin,
  560. "media_id": media_id,
  561. "thumbnail_width": thumbnail_width,
  562. "thumbnail_height": thumbnail_height,
  563. "thumbnail_method": thumbnail_method,
  564. "thumbnail_type": thumbnail_type,
  565. },
  566. values={"thumbnail_length": thumbnail_length},
  567. insertion_values={"filesystem_id": filesystem_id},
  568. desc="store_remote_media_thumbnail",
  569. )
  570. async def get_remote_media_ids(
  571. self, before_ts: int, include_quarantined_media: bool
  572. ) -> List[Dict[str, str]]:
  573. """
  574. Retrieve a list of server name, media ID tuples from the remote media cache.
  575. Args:
  576. before_ts: Only retrieve IDs from media that was either last accessed
  577. (or if never accessed, created) before the given UNIX timestamp in ms.
  578. include_quarantined_media: If False, exclude media IDs from the results that have
  579. been marked as quarantined.
  580. Returns:
  581. A list of tuples containing:
  582. * The server name of homeserver where the media originates from,
  583. * The ID of the media.
  584. """
  585. sql = (
  586. "SELECT media_origin, media_id, filesystem_id"
  587. " FROM remote_media_cache"
  588. " WHERE last_access_ts < ?"
  589. )
  590. if include_quarantined_media is False:
  591. # Only include media that has not been quarantined
  592. sql += """
  593. AND quarantined_by IS NULL
  594. """
  595. return await self.db_pool.execute(
  596. "get_remote_media_ids", self.db_pool.cursor_to_dict, sql, before_ts
  597. )
  598. async def delete_remote_media(self, media_origin: str, media_id: str) -> None:
  599. def delete_remote_media_txn(txn: LoggingTransaction) -> None:
  600. self.db_pool.simple_delete_txn(
  601. txn,
  602. "remote_media_cache",
  603. keyvalues={"media_origin": media_origin, "media_id": media_id},
  604. )
  605. self.db_pool.simple_delete_txn(
  606. txn,
  607. "remote_media_cache_thumbnails",
  608. keyvalues={"media_origin": media_origin, "media_id": media_id},
  609. )
  610. await self.db_pool.runInteraction(
  611. "delete_remote_media", delete_remote_media_txn
  612. )
  613. async def get_expired_url_cache(self, now_ts: int) -> List[str]:
  614. sql = (
  615. "SELECT media_id FROM local_media_repository_url_cache"
  616. " WHERE expires_ts < ?"
  617. " ORDER BY expires_ts ASC"
  618. " LIMIT 500"
  619. )
  620. def _get_expired_url_cache_txn(txn: LoggingTransaction) -> List[str]:
  621. txn.execute(sql, (now_ts,))
  622. return [row[0] for row in txn]
  623. return await self.db_pool.runInteraction(
  624. "get_expired_url_cache", _get_expired_url_cache_txn
  625. )
  626. async def delete_url_cache(self, media_ids: Collection[str]) -> None:
  627. if len(media_ids) == 0:
  628. return
  629. sql = "DELETE FROM local_media_repository_url_cache WHERE media_id = ?"
  630. def _delete_url_cache_txn(txn: LoggingTransaction) -> None:
  631. txn.execute_batch(sql, [(media_id,) for media_id in media_ids])
  632. await self.db_pool.runInteraction("delete_url_cache", _delete_url_cache_txn)
  633. async def get_url_cache_media_before(self, before_ts: int) -> List[str]:
  634. sql = (
  635. "SELECT media_id FROM local_media_repository"
  636. " WHERE created_ts < ? AND url_cache IS NOT NULL"
  637. " ORDER BY created_ts ASC"
  638. " LIMIT 500"
  639. )
  640. def _get_url_cache_media_before_txn(txn: LoggingTransaction) -> List[str]:
  641. txn.execute(sql, (before_ts,))
  642. return [row[0] for row in txn]
  643. return await self.db_pool.runInteraction(
  644. "get_url_cache_media_before", _get_url_cache_media_before_txn
  645. )
  646. async def delete_url_cache_media(self, media_ids: Collection[str]) -> None:
  647. if len(media_ids) == 0:
  648. return
  649. def _delete_url_cache_media_txn(txn: LoggingTransaction) -> None:
  650. sql = "DELETE FROM local_media_repository WHERE media_id = ?"
  651. txn.execute_batch(sql, [(media_id,) for media_id in media_ids])
  652. sql = "DELETE FROM local_media_repository_thumbnails WHERE media_id = ?"
  653. txn.execute_batch(sql, [(media_id,) for media_id in media_ids])
  654. await self.db_pool.runInteraction(
  655. "delete_url_cache_media", _delete_url_cache_media_txn
  656. )