mirror of
https://github.com/penpot/penpot.git
synced 2026-08-28 23:59:00 +00:00
🐛 Fix deduplicated storage overcounting in media-storage-bytes-per-team quote
The SQL query sql:get-media-storage-bytes-per-team used UNION ALL across six SELECT branches that each produce a so_id reference. When deduplication causes multiple file_media_object or team_font_variant rows to point at the same storage_object, UNION ALL counts that objects size once per reference — inflating "used bytes" and causing false :max-quote-reached rejections. Change all five UNION ALL to UNION so that duplicate so_id values are collapsed before the JOIN storage_object / SUM(so.size). Add a test (media-storage-bytes-quote-deduped) that creates one storage_object referenced by two file_media_object rows and asserts the computed usage reflects the deduplicated physical size, not 2x. AI-assisted-by: mimo-v2.5
This commit is contained in:
parent
049205d0fc
commit
6d481eb15b
@ -568,8 +568,8 @@
|
|||||||
WHERE p.team_id = ?
|
WHERE p.team_id = ?
|
||||||
AND fmo.deleted_at IS NULL
|
AND fmo.deleted_at IS NULL
|
||||||
AND f.deleted_at IS NULL
|
AND f.deleted_at IS NULL
|
||||||
UNION ALL
|
UNION
|
||||||
SELECT fmo.thumbnail_id AS so_id
|
SELECT fmo.thumbnail_id AS so_id
|
||||||
FROM file_media_object AS fmo
|
FROM file_media_object AS fmo
|
||||||
JOIN file AS f ON (f.id = fmo.file_id)
|
JOIN file AS f ON (f.id = fmo.file_id)
|
||||||
JOIN project AS p ON (p.id = f.project_id)
|
JOIN project AS p ON (p.id = f.project_id)
|
||||||
@ -577,26 +577,26 @@
|
|||||||
AND fmo.thumbnail_id IS NOT NULL
|
AND fmo.thumbnail_id IS NOT NULL
|
||||||
AND fmo.deleted_at IS NULL
|
AND fmo.deleted_at IS NULL
|
||||||
AND f.deleted_at IS NULL
|
AND f.deleted_at IS NULL
|
||||||
UNION ALL
|
UNION
|
||||||
SELECT v.otf_file_id AS so_id
|
SELECT v.otf_file_id AS so_id
|
||||||
FROM team_font_variant AS v
|
FROM team_font_variant AS v
|
||||||
WHERE v.team_id = ?
|
WHERE v.team_id = ?
|
||||||
AND v.otf_file_id IS NOT NULL
|
AND v.otf_file_id IS NOT NULL
|
||||||
AND v.deleted_at IS NULL
|
AND v.deleted_at IS NULL
|
||||||
UNION ALL
|
UNION
|
||||||
SELECT v.ttf_file_id AS so_id
|
SELECT v.ttf_file_id AS so_id
|
||||||
FROM team_font_variant AS v
|
FROM team_font_variant AS v
|
||||||
WHERE v.team_id = ?
|
WHERE v.team_id = ?
|
||||||
AND v.ttf_file_id IS NOT NULL
|
AND v.ttf_file_id IS NOT NULL
|
||||||
AND v.deleted_at IS NULL
|
AND v.deleted_at IS NULL
|
||||||
UNION ALL
|
UNION
|
||||||
SELECT v.woff1_file_id AS so_id
|
SELECT v.woff1_file_id AS so_id
|
||||||
FROM team_font_variant AS v
|
FROM team_font_variant AS v
|
||||||
WHERE v.team_id = ?
|
WHERE v.team_id = ?
|
||||||
AND v.woff1_file_id IS NOT NULL
|
AND v.woff1_file_id IS NOT NULL
|
||||||
AND v.deleted_at IS NULL
|
AND v.deleted_at IS NULL
|
||||||
UNION ALL
|
UNION
|
||||||
SELECT v.woff2_file_id AS so_id
|
SELECT v.woff2_file_id AS so_id
|
||||||
FROM team_font_variant AS v
|
FROM team_font_variant AS v
|
||||||
WHERE v.team_id = ?
|
WHERE v.team_id = ?
|
||||||
AND v.woff2_file_id IS NOT NULL
|
AND v.woff2_file_id IS NOT NULL
|
||||||
|
|||||||
@ -400,6 +400,47 @@
|
|||||||
;; total=0, incr=500, best quote=1000 → 0+500 <= 1000 → ok
|
;; total=0, incr=500, best quote=1000 → 0+500 <= 1000 → ok
|
||||||
(check-ok! "allowed by team+profile quote"))))
|
(check-ok! "allowed by team+profile quote"))))
|
||||||
|
|
||||||
|
(t/deftest media-storage-bytes-quote-deduped
|
||||||
|
(with-mocks [mock {:target 'app.config/get
|
||||||
|
:return (th/config-get-mock
|
||||||
|
{:quotes-media-storage-bytes-per-team 1100})}]
|
||||||
|
|
||||||
|
(let [prof (th/create-profile* 1)
|
||||||
|
team-id (:default-team-id prof)
|
||||||
|
proj (th/create-project* 1 {:profile-id (:id prof)
|
||||||
|
:team-id team-id})
|
||||||
|
file1 (th/create-file* 1 {:profile-id (:id prof)
|
||||||
|
:project-id (:id proj)
|
||||||
|
:is-shared false})
|
||||||
|
file2 (th/create-file* 2 {:profile-id (:id prof)
|
||||||
|
:project-id (:id proj)
|
||||||
|
:is-shared false})
|
||||||
|
|
||||||
|
;; One physical storage object of 500 bytes
|
||||||
|
so-id (uuid/random)
|
||||||
|
_ (th/db-insert! :storage-object {:id so-id
|
||||||
|
:size 500
|
||||||
|
:backend "test"})
|
||||||
|
|
||||||
|
;; Two file_media_object rows pointing at the SAME storage object
|
||||||
|
;; (simulates the deduplication path: same content uploaded twice)
|
||||||
|
_ (th/create-file-media-object*
|
||||||
|
{:file-id (:id file1) :media-id so-id
|
||||||
|
:name "icon" :mtype "image/svg+xml"})
|
||||||
|
_ (th/create-file-media-object*
|
||||||
|
{:file-id (:id file2) :media-id so-id
|
||||||
|
:name "icon" :mtype "image/svg+xml"})
|
||||||
|
|
||||||
|
data {::quotes/id ::quotes/media-storage-bytes-per-team
|
||||||
|
::quotes/profile-id (:id prof)
|
||||||
|
::quotes/team-id team-id
|
||||||
|
::quotes/incr 200}]
|
||||||
|
|
||||||
|
;; Physical size is 500. With UNION (correct), total=500, 500+200=700 ≤ 1100 → ok.
|
||||||
|
;; With UNION ALL (buggy), total=1000, 1000+200=1200 > 1100 → rejected.
|
||||||
|
(quotes/check! th/*system* data)
|
||||||
|
(t/is (true? true) "deduped storage counted once, under quota"))))
|
||||||
|
|
||||||
(t/deftest media-upload-enforces-storage-quote
|
(t/deftest media-upload-enforces-storage-quote
|
||||||
(with-mocks [mock {:target 'app.config/get
|
(with-mocks [mock {:target 'app.config/get
|
||||||
:return (th/config-get-mock
|
:return (th/config-get-mock
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user