Mercurial > prosody-modules
comparison mod_muc_cache_media/mod_muc_cache_media.lua @ 6417:dcc8d4af8d4f
mod_muc_cache_media: move to storage that matches RFC 6920
| author | Stephen Paul Weber <singpolyma@singpolyma.net> |
|---|---|
| date | Tue, 24 Feb 2026 11:57:09 -0500 |
| parents | 420aaf113272 |
| children | 98e06fc5680f |
comparison
equal
deleted
inserted
replaced
| 6416:2511bf07b8d2 | 6417:dcc8d4af8d4f |
|---|---|
| 8 local lfs = require "lfs" | 8 local lfs = require "lfs" |
| 9 local hashes = require "util.hashes" | 9 local hashes = require "util.hashes" |
| 10 local jid = require "util.jid" | 10 local jid = require "util.jid" |
| 11 | 11 |
| 12 local storage_path = module:get_option_string("muc_media_store_path", "/var/lib/prosody/muc-media") | 12 local storage_path = module:get_option_string("muc_media_store_path", "/var/lib/prosody/muc-media") |
| 13 local public_base_url = module:get_option_string("muc_media_public_base", "https://cache.example.com") | 13 local public_base_url = module:get_option_string("muc_media_public_base") |
| 14 local files_have_extensions = module:get_option_boolean("muc_media_files_have_extensions", true) | |
| 15 local max_size = module:get_option_number("muc_media_max_size", 10 * 1024 * 1024) | 14 local max_size = module:get_option_number("muc_media_max_size", 10 * 1024 * 1024) |
| 16 | 15 |
| 17 lfs.mkdir(storage_path) | 16 lfs.mkdir(storage_path) |
| 18 | 17 lfs.mkdir(storage_path.."/sha-256") |
| 19 local mime_extensions = { | 18 lfs.mkdir(storage_path.."/sha3-256") |
| 20 ["image/jpeg"] = ".jpg", | |
| 21 ["image/jpg"] = ".jpg", | |
| 22 ["image/png"] = ".png", | |
| 23 ["image/gif"] = ".gif", | |
| 24 ["image/webp"] = ".webp", | |
| 25 ["image/avif"] = ".avif", | |
| 26 ["video/mp4"] = ".mp4", | |
| 27 ["video/webm"] = ".webm", | |
| 28 ["audio/mpeg"] = ".mp3", | |
| 29 ["audio/ogg"] = ".ogg", | |
| 30 } | |
| 31 | |
| 32 local function extension_from_content_type(content_type) | |
| 33 if not content_type then return ".bin" end | |
| 34 local clean = content_type:match("^[^;]+") | |
| 35 clean = clean and clean:lower() | |
| 36 return mime_extensions[clean] or ".bin" | |
| 37 end | |
| 38 | 19 |
| 39 local function ipv4_to_number(ip) | 20 local function ipv4_to_number(ip) |
| 40 local a,b,c,d = ip:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$") | 21 local a,b,c,d = ip:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$") |
| 41 if not a then return nil end | 22 if not a then return nil end |
| 42 return (tonumber(a) << 24) | (tonumber(b) << 16) | (tonumber(c) << 8) | tonumber(d) | 23 return (tonumber(a) << 24) | (tonumber(b) << 16) | (tonumber(c) << 8) | tonumber(d) |
| 158 if not err and tmp_path then | 139 if not err and tmp_path then |
| 159 local sha2 = sha256_file(tmp_path) | 140 local sha2 = sha256_file(tmp_path) |
| 160 local sha3 = sha3_256_file(tmp_path) | 141 local sha3 = sha3_256_file(tmp_path) |
| 161 local size = file_size(tmp_path) | 142 local size = file_size(tmp_path) |
| 162 local name = filename_from_url(original_url) | 143 local name = filename_from_url(original_url) |
| 163 if sha2 then | 144 if sha2 and sha3 then |
| 164 local ext = extension_from_content_type(content_type) | 145 local final_name = "sha3-256/"..sha3:gsub("+","-"):gsub("/","_"):gsub("=","") |
| 165 local file_name = sha2:gsub("+","-"):gsub("/","_"):gsub("=","") | 146 local final_path = storage_path.."/"..final_name |
| 166 local url_name = file_name..ext | |
| 167 if files_have_extensions then | |
| 168 file_name = file_name..ext | |
| 169 end | |
| 170 local final_path = storage_path.."/"..file_name | |
| 171 if not lfs.attributes(final_path) then | 147 if not lfs.attributes(final_path) then |
| 172 os.rename(tmp_path, final_path) | 148 os.rename(tmp_path, final_path) |
| 173 else | 149 else |
| 150 lfs.touch(final_path) | |
| 174 os.remove(tmp_path) | 151 os.remove(tmp_path) |
| 175 end | 152 end |
| 176 url_map[original_url] = { local_url = public_base_url.."/"..url_name, path=final_path, sha2=sha2, sha3=sha3, size=size, name=name, content_type=content_type } | 153 local sha2_path = storage_path.."/sha-256/"..sha2:gsub("+","-"):gsub("/","_"):gsub("=","") |
| 154 if not lfs.attributes(sha2_path) then | |
| 155 lfs.link(final_path, sha2_path, true) | |
| 156 end | |
| 157 url_map[original_url] = { local_url = public_base_url..final_name.."?ct="..content_type, path=final_path, sha2=sha2, sha3=sha3, size=size, name=name, content_type=content_type } | |
| 177 else | 158 else |
| 178 os.remove(tmp_path) | 159 os.remove(tmp_path) |
| 179 end | 160 end |
| 180 end | 161 end |
| 181 end | 162 end |
| 245 file_tag:add_child(st.stanza("size"):text(tostring(data.size))) | 226 file_tag:add_child(st.stanza("size"):text(tostring(data.size))) |
| 246 end | 227 end |
| 247 -- Fill SHA256 hash | 228 -- Fill SHA256 hash |
| 248 local sha2_tag = nil | 229 local sha2_tag = nil |
| 249 for htag in file_tag:childtags("hash","urn:xmpp:hashes:2") do | 230 for htag in file_tag:childtags("hash","urn:xmpp:hashes:2") do |
| 250 if htag.attr.algo=="sha2-256" then sha2_tag = htag end | 231 if htag.attr.algo=="sha-256" then sha2_tag = htag end |
| 251 end | 232 end |
| 252 if not sha2_tag then | 233 if not sha2_tag then |
| 253 file_tag:add_child(st.stanza("hash",{ xmlns="urn:xmpp:hashes:2", algo="sha2-256" }):text(data.sha2)) | 234 file_tag:add_child(st.stanza("hash",{ xmlns="urn:xmpp:hashes:2", algo="sha-256" }):text(data.sha2)) |
| 254 end | 235 end |
| 255 if data.sha3 then | 236 -- Fill SHA3-256 hash |
| 256 -- Fill SHA3-256 hash | 237 local sha3_tag = nil |
| 257 local sha3_tag = nil | 238 for htag in file_tag:childtags("hash","urn:xmpp:hashes:2") do |
| 258 for htag in file_tag:childtags("hash","urn:xmpp:hashes:2") do | 239 if htag.attr.algo=="sha3-256" then sha3_tag = htag end |
| 259 if htag.attr.algo=="sha3-256" then sha3_tag = htag end | 240 end |
| 260 end | 241 if not sha3_tag then |
| 261 if not sha3_tag then | 242 file_tag:add_child(st.stanza("hash",{ xmlns="urn:xmpp:hashes:2", algo="sha3-256" }):text(data.sha3)) |
| 262 file_tag:add_child(st.stanza("hash",{ xmlns="urn:xmpp:hashes:2", algo="sha3-256" }):text(data.sha3)) | |
| 263 end | |
| 264 end | 243 end |
| 265 end | 244 end |
| 266 end | 245 end |
| 267 end | 246 end |
| 268 end | 247 end |
| 273 local ms = st.stanza("media-sharing",{ xmlns="urn:xmpp:sims:1" }) | 252 local ms = st.stanza("media-sharing",{ xmlns="urn:xmpp:sims:1" }) |
| 274 local file_tag = st.stanza("file",{ xmlns="urn:xmpp:jingle:apps:file-transfer:5" }) | 253 local file_tag = st.stanza("file",{ xmlns="urn:xmpp:jingle:apps:file-transfer:5" }) |
| 275 file_tag:add_child(st.stanza("media-type"):text(data.content_type or "application/octet-stream")) | 254 file_tag:add_child(st.stanza("media-type"):text(data.content_type or "application/octet-stream")) |
| 276 file_tag:add_child(st.stanza("name"):text(data.name)) | 255 file_tag:add_child(st.stanza("name"):text(data.name)) |
| 277 file_tag:add_child(st.stanza("size"):text(tostring(data.size))) | 256 file_tag:add_child(st.stanza("size"):text(tostring(data.size))) |
| 278 file_tag:add_child(st.stanza("hash",{ xmlns="urn:xmpp:hashes:2", algo="sha2-256" }):text(data.sha2)) | 257 file_tag:add_child(st.stanza("hash",{ xmlns="urn:xmpp:hashes:2", algo="sha-256" }):text(data.sha2)) |
| 279 if data.sha3 then file_tag:add_child(st.stanza("hash",{ xmlns="urn:xmpp:hashes:2", algo="sha3-256" }):text(data.sha3)) end | 258 file_tag:add_child(st.stanza("hash",{ xmlns="urn:xmpp:hashes:2", algo="sha3-256" }):text(data.sha3)) |
| 280 ms:add_child(file_tag) | 259 ms:add_child(file_tag) |
| 281 local sources = st.stanza("sources") | 260 local sources = st.stanza("sources") |
| 282 sources:add_child(st.stanza("reference",{ xmlns="urn:xmpp:reference:0", type="data", uri=data.local_url })) | 261 sources:add_child(st.stanza("reference",{ xmlns="urn:xmpp:reference:0", type="data", uri=data.local_url })) |
| 283 ms:add_child(sources) | 262 ms:add_child(sources) |
| 284 ref:add_child(ms) | 263 ref:add_child(ms) |
| 308 | 287 |
| 309 for id, stanza, when in items do | 288 for id, stanza, when in items do |
| 310 -- Extract SIMS hashes | 289 -- Extract SIMS hashes |
| 311 for ref in stanza:childtags("reference", "urn:xmpp:reference:0") do | 290 for ref in stanza:childtags("reference", "urn:xmpp:reference:0") do |
| 312 for ms in ref:childtags("media-sharing", "urn:xmpp:sims:1") do | 291 for ms in ref:childtags("media-sharing", "urn:xmpp:sims:1") do |
| 313 local file_tag = ms:get_child("file", "urn:xmpp:jingle:apps:file-transfer:5") | 292 for file_tag in ms:childtags("file", "urn:xmpp:jingle:apps:file-transfer:5") do |
| 314 if file_tag then | |
| 315 for hash_tag in file_tag:childtags("hash", "urn:xmpp:hashes:2") do | 293 for hash_tag in file_tag:childtags("hash", "urn:xmpp:hashes:2") do |
| 316 if hash_tag.attr.algo == "sha2-256" then | 294 if hash_tag.attr.algo == "sha3-256" then |
| 317 local sha2 = hash_tag:get_text() | 295 local sha3 = hash_tag:get_text() |
| 318 if sha2 then | 296 if sha3 then |
| 319 local prefix = sha2:gsub("+","-"):gsub("/","_"):gsub("=","") | 297 local file = sha3:gsub("+","-"):gsub("/","_"):gsub("=","") |
| 320 if files_have_extensions then | 298 local path = storage_path .. "/sha3-256/" .. file |
| 321 -- Files are named: hash + extension | 299 if lfs.attributes(path) then |
| 322 for file in lfs.dir(storage_path) do | 300 os.remove(path) |
| 323 if file:match("^" .. prefix) then | 301 module:log("debug", |
| 324 local path = storage_path .. "/" .. file | 302 "Deleted cached media %s for moderated message %s", |
| 325 if lfs.attributes(path) then | 303 file, stanza_id |
| 326 os.remove(path) | 304 ) |
| 327 module:log("debug", | |
| 328 "Deleted cached media %s for moderated message %s", | |
| 329 file, stanza_id | |
| 330 ) | |
| 331 end | |
| 332 end | |
| 333 end | |
| 334 else | |
| 335 local path = storage_path .. "/" .. prefix | |
| 336 if lfs.attributes(path) then | |
| 337 os.remove(path) | |
| 338 module:log("debug", | |
| 339 "Deleted cached media %s for moderated message %s", | |
| 340 file, stanza_id | |
| 341 ) | |
| 342 end | |
| 343 end | 305 end |
| 344 end | 306 end |
| 345 end | 307 end |
| 346 end | 308 end |
| 347 end | 309 end |
