test_media.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. # Copyright 2020 Dirk Klimpel
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import json
  15. import os
  16. from binascii import unhexlify
  17. import synapse.rest.admin
  18. from synapse.api.errors import Codes
  19. from synapse.rest.client.v1 import login, profile, room
  20. from synapse.rest.media.v1.filepath import MediaFilePaths
  21. from tests import unittest
  22. from tests.server import FakeSite, make_request
  23. class DeleteMediaByIDTestCase(unittest.HomeserverTestCase):
  24. servlets = [
  25. synapse.rest.admin.register_servlets,
  26. synapse.rest.admin.register_servlets_for_media_repo,
  27. login.register_servlets,
  28. ]
  29. def prepare(self, reactor, clock, hs):
  30. self.media_repo = hs.get_media_repository_resource()
  31. self.server_name = hs.hostname
  32. self.admin_user = self.register_user("admin", "pass", admin=True)
  33. self.admin_user_tok = self.login("admin", "pass")
  34. self.filepaths = MediaFilePaths(hs.config.media_store_path)
  35. def test_no_auth(self):
  36. """
  37. Try to delete media without authentication.
  38. """
  39. url = "/_synapse/admin/v1/media/%s/%s" % (self.server_name, "12345")
  40. channel = self.make_request("DELETE", url, b"{}")
  41. self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"])
  42. self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])
  43. def test_requester_is_no_admin(self):
  44. """
  45. If the user is not a server admin, an error is returned.
  46. """
  47. self.other_user = self.register_user("user", "pass")
  48. self.other_user_token = self.login("user", "pass")
  49. url = "/_synapse/admin/v1/media/%s/%s" % (self.server_name, "12345")
  50. channel = self.make_request(
  51. "DELETE",
  52. url,
  53. access_token=self.other_user_token,
  54. )
  55. self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"])
  56. self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
  57. def test_media_does_not_exist(self):
  58. """
  59. Tests that a lookup for a media that does not exist returns a 404
  60. """
  61. url = "/_synapse/admin/v1/media/%s/%s" % (self.server_name, "12345")
  62. channel = self.make_request(
  63. "DELETE",
  64. url,
  65. access_token=self.admin_user_tok,
  66. )
  67. self.assertEqual(404, channel.code, msg=channel.json_body)
  68. self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"])
  69. def test_media_is_not_local(self):
  70. """
  71. Tests that a lookup for a media that is not a local returns a 400
  72. """
  73. url = "/_synapse/admin/v1/media/%s/%s" % ("unknown_domain", "12345")
  74. channel = self.make_request(
  75. "DELETE",
  76. url,
  77. access_token=self.admin_user_tok,
  78. )
  79. self.assertEqual(400, channel.code, msg=channel.json_body)
  80. self.assertEqual("Can only delete local media", channel.json_body["error"])
  81. def test_delete_media(self):
  82. """
  83. Tests that delete a media is successfully
  84. """
  85. download_resource = self.media_repo.children[b"download"]
  86. upload_resource = self.media_repo.children[b"upload"]
  87. image_data = unhexlify(
  88. b"89504e470d0a1a0a0000000d4948445200000001000000010806"
  89. b"0000001f15c4890000000a49444154789c63000100000500010d"
  90. b"0a2db40000000049454e44ae426082"
  91. )
  92. # Upload some media into the room
  93. response = self.helper.upload_media(
  94. upload_resource, image_data, tok=self.admin_user_tok, expect_code=200
  95. )
  96. # Extract media ID from the response
  97. server_and_media_id = response["content_uri"][6:] # Cut off 'mxc://'
  98. server_name, media_id = server_and_media_id.split("/")
  99. self.assertEqual(server_name, self.server_name)
  100. # Attempt to access media
  101. channel = make_request(
  102. self.reactor,
  103. FakeSite(download_resource),
  104. "GET",
  105. server_and_media_id,
  106. shorthand=False,
  107. access_token=self.admin_user_tok,
  108. )
  109. # Should be successful
  110. self.assertEqual(
  111. 200,
  112. channel.code,
  113. msg=(
  114. "Expected to receive a 200 on accessing media: %s" % server_and_media_id
  115. ),
  116. )
  117. # Test if the file exists
  118. local_path = self.filepaths.local_media_filepath(media_id)
  119. self.assertTrue(os.path.exists(local_path))
  120. url = "/_synapse/admin/v1/media/%s/%s" % (self.server_name, media_id)
  121. # Delete media
  122. channel = self.make_request(
  123. "DELETE",
  124. url,
  125. access_token=self.admin_user_tok,
  126. )
  127. self.assertEqual(200, channel.code, msg=channel.json_body)
  128. self.assertEqual(1, channel.json_body["total"])
  129. self.assertEqual(
  130. media_id,
  131. channel.json_body["deleted_media"][0],
  132. )
  133. # Attempt to access media
  134. channel = make_request(
  135. self.reactor,
  136. FakeSite(download_resource),
  137. "GET",
  138. server_and_media_id,
  139. shorthand=False,
  140. access_token=self.admin_user_tok,
  141. )
  142. self.assertEqual(
  143. 404,
  144. channel.code,
  145. msg=(
  146. "Expected to receive a 404 on accessing deleted media: %s"
  147. % server_and_media_id
  148. ),
  149. )
  150. # Test if the file is deleted
  151. self.assertFalse(os.path.exists(local_path))
  152. class DeleteMediaByDateSizeTestCase(unittest.HomeserverTestCase):
  153. servlets = [
  154. synapse.rest.admin.register_servlets,
  155. synapse.rest.admin.register_servlets_for_media_repo,
  156. login.register_servlets,
  157. profile.register_servlets,
  158. room.register_servlets,
  159. ]
  160. def prepare(self, reactor, clock, hs):
  161. self.media_repo = hs.get_media_repository_resource()
  162. self.server_name = hs.hostname
  163. self.admin_user = self.register_user("admin", "pass", admin=True)
  164. self.admin_user_tok = self.login("admin", "pass")
  165. self.filepaths = MediaFilePaths(hs.config.media_store_path)
  166. self.url = "/_synapse/admin/v1/media/%s/delete" % self.server_name
  167. def test_no_auth(self):
  168. """
  169. Try to delete media without authentication.
  170. """
  171. channel = self.make_request("POST", self.url, b"{}")
  172. self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"])
  173. self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])
  174. def test_requester_is_no_admin(self):
  175. """
  176. If the user is not a server admin, an error is returned.
  177. """
  178. self.other_user = self.register_user("user", "pass")
  179. self.other_user_token = self.login("user", "pass")
  180. channel = self.make_request(
  181. "POST",
  182. self.url,
  183. access_token=self.other_user_token,
  184. )
  185. self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"])
  186. self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
  187. def test_media_is_not_local(self):
  188. """
  189. Tests that a lookup for media that is not local returns a 400
  190. """
  191. url = "/_synapse/admin/v1/media/%s/delete" % "unknown_domain"
  192. channel = self.make_request(
  193. "POST",
  194. url + "?before_ts=1234",
  195. access_token=self.admin_user_tok,
  196. )
  197. self.assertEqual(400, channel.code, msg=channel.json_body)
  198. self.assertEqual("Can only delete local media", channel.json_body["error"])
  199. def test_missing_parameter(self):
  200. """
  201. If the parameter `before_ts` is missing, an error is returned.
  202. """
  203. channel = self.make_request(
  204. "POST",
  205. self.url,
  206. access_token=self.admin_user_tok,
  207. )
  208. self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
  209. self.assertEqual(Codes.MISSING_PARAM, channel.json_body["errcode"])
  210. self.assertEqual(
  211. "Missing integer query parameter b'before_ts'", channel.json_body["error"]
  212. )
  213. def test_invalid_parameter(self):
  214. """
  215. If parameters are invalid, an error is returned.
  216. """
  217. channel = self.make_request(
  218. "POST",
  219. self.url + "?before_ts=-1234",
  220. access_token=self.admin_user_tok,
  221. )
  222. self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
  223. self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
  224. self.assertEqual(
  225. "Query parameter before_ts must be a string representing a positive integer.",
  226. channel.json_body["error"],
  227. )
  228. channel = self.make_request(
  229. "POST",
  230. self.url + "?before_ts=1234&size_gt=-1234",
  231. access_token=self.admin_user_tok,
  232. )
  233. self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
  234. self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
  235. self.assertEqual(
  236. "Query parameter size_gt must be a string representing a positive integer.",
  237. channel.json_body["error"],
  238. )
  239. channel = self.make_request(
  240. "POST",
  241. self.url + "?before_ts=1234&keep_profiles=not_bool",
  242. access_token=self.admin_user_tok,
  243. )
  244. self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
  245. self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"])
  246. self.assertEqual(
  247. "Boolean query parameter b'keep_profiles' must be one of ['true', 'false']",
  248. channel.json_body["error"],
  249. )
  250. def test_delete_media_never_accessed(self):
  251. """
  252. Tests that media deleted if it is older than `before_ts` and never accessed
  253. `last_access_ts` is `NULL` and `created_ts` < `before_ts`
  254. """
  255. # upload and do not access
  256. server_and_media_id = self._create_media()
  257. self.pump(1.0)
  258. # test that the file exists
  259. media_id = server_and_media_id.split("/")[1]
  260. local_path = self.filepaths.local_media_filepath(media_id)
  261. self.assertTrue(os.path.exists(local_path))
  262. # timestamp after upload/create
  263. now_ms = self.clock.time_msec()
  264. channel = self.make_request(
  265. "POST",
  266. self.url + "?before_ts=" + str(now_ms),
  267. access_token=self.admin_user_tok,
  268. )
  269. self.assertEqual(200, channel.code, msg=channel.json_body)
  270. self.assertEqual(1, channel.json_body["total"])
  271. self.assertEqual(
  272. media_id,
  273. channel.json_body["deleted_media"][0],
  274. )
  275. self._access_media(server_and_media_id, False)
  276. def test_keep_media_by_date(self):
  277. """
  278. Tests that media is not deleted if it is newer than `before_ts`
  279. """
  280. # timestamp before upload
  281. now_ms = self.clock.time_msec()
  282. server_and_media_id = self._create_media()
  283. self._access_media(server_and_media_id)
  284. channel = self.make_request(
  285. "POST",
  286. self.url + "?before_ts=" + str(now_ms),
  287. access_token=self.admin_user_tok,
  288. )
  289. self.assertEqual(200, channel.code, msg=channel.json_body)
  290. self.assertEqual(0, channel.json_body["total"])
  291. self._access_media(server_and_media_id)
  292. # timestamp after upload
  293. now_ms = self.clock.time_msec()
  294. channel = self.make_request(
  295. "POST",
  296. self.url + "?before_ts=" + str(now_ms),
  297. access_token=self.admin_user_tok,
  298. )
  299. self.assertEqual(200, channel.code, msg=channel.json_body)
  300. self.assertEqual(1, channel.json_body["total"])
  301. self.assertEqual(
  302. server_and_media_id.split("/")[1],
  303. channel.json_body["deleted_media"][0],
  304. )
  305. self._access_media(server_and_media_id, False)
  306. def test_keep_media_by_size(self):
  307. """
  308. Tests that media is not deleted if its size is smaller than or equal
  309. to `size_gt`
  310. """
  311. server_and_media_id = self._create_media()
  312. self._access_media(server_and_media_id)
  313. now_ms = self.clock.time_msec()
  314. channel = self.make_request(
  315. "POST",
  316. self.url + "?before_ts=" + str(now_ms) + "&size_gt=67",
  317. access_token=self.admin_user_tok,
  318. )
  319. self.assertEqual(200, channel.code, msg=channel.json_body)
  320. self.assertEqual(0, channel.json_body["total"])
  321. self._access_media(server_and_media_id)
  322. now_ms = self.clock.time_msec()
  323. channel = self.make_request(
  324. "POST",
  325. self.url + "?before_ts=" + str(now_ms) + "&size_gt=66",
  326. access_token=self.admin_user_tok,
  327. )
  328. self.assertEqual(200, channel.code, msg=channel.json_body)
  329. self.assertEqual(1, channel.json_body["total"])
  330. self.assertEqual(
  331. server_and_media_id.split("/")[1],
  332. channel.json_body["deleted_media"][0],
  333. )
  334. self._access_media(server_and_media_id, False)
  335. def test_keep_media_by_user_avatar(self):
  336. """
  337. Tests that we do not delete media if is used as a user avatar
  338. Tests parameter `keep_profiles`
  339. """
  340. server_and_media_id = self._create_media()
  341. self._access_media(server_and_media_id)
  342. # set media as avatar
  343. channel = self.make_request(
  344. "PUT",
  345. "/profile/%s/avatar_url" % (self.admin_user,),
  346. content=json.dumps({"avatar_url": "mxc://%s" % (server_and_media_id,)}),
  347. access_token=self.admin_user_tok,
  348. )
  349. self.assertEqual(200, channel.code, msg=channel.json_body)
  350. now_ms = self.clock.time_msec()
  351. channel = self.make_request(
  352. "POST",
  353. self.url + "?before_ts=" + str(now_ms) + "&keep_profiles=true",
  354. access_token=self.admin_user_tok,
  355. )
  356. self.assertEqual(200, channel.code, msg=channel.json_body)
  357. self.assertEqual(0, channel.json_body["total"])
  358. self._access_media(server_and_media_id)
  359. now_ms = self.clock.time_msec()
  360. channel = self.make_request(
  361. "POST",
  362. self.url + "?before_ts=" + str(now_ms) + "&keep_profiles=false",
  363. access_token=self.admin_user_tok,
  364. )
  365. self.assertEqual(200, channel.code, msg=channel.json_body)
  366. self.assertEqual(1, channel.json_body["total"])
  367. self.assertEqual(
  368. server_and_media_id.split("/")[1],
  369. channel.json_body["deleted_media"][0],
  370. )
  371. self._access_media(server_and_media_id, False)
  372. def test_keep_media_by_room_avatar(self):
  373. """
  374. Tests that we do not delete media if it is used as a room avatar
  375. Tests parameter `keep_profiles`
  376. """
  377. server_and_media_id = self._create_media()
  378. self._access_media(server_and_media_id)
  379. # set media as room avatar
  380. room_id = self.helper.create_room_as(self.admin_user, tok=self.admin_user_tok)
  381. channel = self.make_request(
  382. "PUT",
  383. "/rooms/%s/state/m.room.avatar" % (room_id,),
  384. content=json.dumps({"url": "mxc://%s" % (server_and_media_id,)}),
  385. access_token=self.admin_user_tok,
  386. )
  387. self.assertEqual(200, channel.code, msg=channel.json_body)
  388. now_ms = self.clock.time_msec()
  389. channel = self.make_request(
  390. "POST",
  391. self.url + "?before_ts=" + str(now_ms) + "&keep_profiles=true",
  392. access_token=self.admin_user_tok,
  393. )
  394. self.assertEqual(200, channel.code, msg=channel.json_body)
  395. self.assertEqual(0, channel.json_body["total"])
  396. self._access_media(server_and_media_id)
  397. now_ms = self.clock.time_msec()
  398. channel = self.make_request(
  399. "POST",
  400. self.url + "?before_ts=" + str(now_ms) + "&keep_profiles=false",
  401. access_token=self.admin_user_tok,
  402. )
  403. self.assertEqual(200, channel.code, msg=channel.json_body)
  404. self.assertEqual(1, channel.json_body["total"])
  405. self.assertEqual(
  406. server_and_media_id.split("/")[1],
  407. channel.json_body["deleted_media"][0],
  408. )
  409. self._access_media(server_and_media_id, False)
  410. def _create_media(self):
  411. """
  412. Create a media and return media_id and server_and_media_id
  413. """
  414. upload_resource = self.media_repo.children[b"upload"]
  415. # file size is 67 Byte
  416. image_data = unhexlify(
  417. b"89504e470d0a1a0a0000000d4948445200000001000000010806"
  418. b"0000001f15c4890000000a49444154789c63000100000500010d"
  419. b"0a2db40000000049454e44ae426082"
  420. )
  421. # Upload some media into the room
  422. response = self.helper.upload_media(
  423. upload_resource, image_data, tok=self.admin_user_tok, expect_code=200
  424. )
  425. # Extract media ID from the response
  426. server_and_media_id = response["content_uri"][6:] # Cut off 'mxc://'
  427. server_name = server_and_media_id.split("/")[0]
  428. # Check that new media is a local and not remote
  429. self.assertEqual(server_name, self.server_name)
  430. return server_and_media_id
  431. def _access_media(self, server_and_media_id, expect_success=True):
  432. """
  433. Try to access a media and check the result
  434. """
  435. download_resource = self.media_repo.children[b"download"]
  436. media_id = server_and_media_id.split("/")[1]
  437. local_path = self.filepaths.local_media_filepath(media_id)
  438. channel = make_request(
  439. self.reactor,
  440. FakeSite(download_resource),
  441. "GET",
  442. server_and_media_id,
  443. shorthand=False,
  444. access_token=self.admin_user_tok,
  445. )
  446. if expect_success:
  447. self.assertEqual(
  448. 200,
  449. channel.code,
  450. msg=(
  451. "Expected to receive a 200 on accessing media: %s"
  452. % server_and_media_id
  453. ),
  454. )
  455. # Test that the file exists
  456. self.assertTrue(os.path.exists(local_path))
  457. else:
  458. self.assertEqual(
  459. 404,
  460. channel.code,
  461. msg=(
  462. "Expected to receive a 404 on accessing deleted media: %s"
  463. % (server_and_media_id)
  464. ),
  465. )
  466. # Test that the file is deleted
  467. self.assertFalse(os.path.exists(local_path))