// Matrix Construct // // Copyright (C) Matrix Construct Developers, Authors & Contributors // Copyright (C) 2016-2018 Jason Volk // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice is present in all copies. The // full license for this software is available in the LICENSE file. #include "media.h" ircd::mapi::header IRCD_MODULE { "11.7 :Content respository", ircd::m::media::init, ircd::m::media::fini }; struct ircd::m::media::magick { module modules { "magick" }; }; void ircd::m::media::init() { static const std::string dbopts; ::media = std::make_shared("media", dbopts, media_description); ::blocks = db::column{*::media, "blocks"}; // The conf setter callbacks must be manually executed after // the database was just loaded to set the cache size. conf::reset("ircd.media.blocks.cache.size"); conf::reset("ircd.media.blocks.cache_comp.size"); // conditions to load the magick.so module const bool enable_magick { // used by the thumbnailer thumbnail::enable // support is available && mods::available("magick") }; if(enable_magick) magick_support.reset(new media::magick{}); else log::warning { media_log, "GraphicsMagick support is disabled or unavailable." }; } void ircd::m::media::fini() { magick_support.reset(); // The database close contains pthread_join()'s within RocksDB which // deadlock under certain conditions when called during a dlclose() // (i.e static destruction of this module). Therefor we must manually // close the db here first. ::media = std::shared_ptr{}; } decltype(media_log) media_log { "m.media" }; decltype(media_blocks_cache_enable) media_blocks_cache_enable { { "name", "ircd.media.blocks.cache.enable" }, { "default", true }, }; decltype(media_blocks_cache_comp_enable) media_blocks_cache_comp_enable { { "name", "ircd.media.blocks.cache_comp.enable" }, { "default", false }, }; // Blocks column decltype(media_blocks_descriptor) media_blocks_descriptor { // name "blocks", // explain R"( Key-value store of blocks belonging to files. The key is a hash of the block. The key is plaintext sha256-b58 and the block is binary up to 32768 bytes. )", // typing { typeid(string_view), typeid(string_view) }, {}, // options {}, // comparaor {}, // prefix transform false, // drop column bool(media_blocks_cache_enable)? -1 : 0, bool(media_blocks_cache_comp_enable)? -1 : 0, // bloom_bits 10, // expect hit true, // block_size 32_KiB, // meta block size 512, // compression ""s // no compression }; decltype(media_description) media_description { { "default" }, // requirement of RocksDB media_blocks_descriptor, }; decltype(media_blocks_cache_size) media_blocks_cache_size { { { "name", "ircd.media.blocks.cache.size" }, { "default", long(48_MiB) }, }, [] { if(!blocks) return; const size_t &value{media_blocks_cache_size}; db::capacity(db::cache(blocks), value); } }; decltype(media_blocks_cache_comp_size) media_blocks_cache_comp_size { { { "name", "ircd.media.blocks.cache_comp.size" }, { "default", long(16_MiB) }, }, [] { if(!blocks) return; const size_t &value{media_blocks_cache_comp_size}; db::capacity(db::cache_compressed(blocks), value); } }; decltype(media) media; decltype(blocks) blocks; decltype(magick_support) magick_support; std::set downloading; ctx::dock downloading_dock; m::room::id::buf download(const string_view &server, const string_view &mediaid, const m::user::id &user_id, const net::hostport &remote) { const m::room::id::buf room_id { file_room_id(server, mediaid) }; thread_local char rembuf[256]; if(remote && my_host(string(rembuf, remote))) return room_id; if(!remote && my_host(server)) return room_id; download(server, mediaid, user_id, remote, room_id); return room_id; } m::room download(const string_view &server, const string_view &mediaid, const m::user::id &user_id, const net::hostport &remote, const m::room::id &room_id) try { auto iit { downloading.emplace(room_id) }; if(!iit.second) { downloading_dock.wait([&room_id] { return !downloading.count(room_id); }); return room_id; } const unwind uw{[&iit] { downloading.erase(iit.first); downloading_dock.notify_all(); }}; if(exists(room_id)) return room_id; const unique_buffer buf { 16_KiB }; const auto pair { download(buf, server, mediaid, remote) }; const auto &head { pair.first }; const const_buffer &content { pair.second }; char mime_type_buf[64]; const auto &content_type { magic::mime(mime_type_buf, content) }; if(content_type != head.content_type) log::dwarning { media_log, "Server %s claims thumbnail %s is '%s' but we think it is '%s'", string(remote), mediaid, head.content_type, content_type }; m::vm::copts vmopts; vmopts.history = false; const m::room room { room_id, &vmopts }; create(room, user_id, "file"); const unwind::exceptional purge{[&room] { m::room::purge(room); }}; const size_t written { write_file(room, user_id, content, content_type) }; return room; } catch(const ircd::server::unavailable &e) { thread_local char rembuf[256]; throw m::error { http::BAD_GATEWAY, "M_MEDIA_UNAVAILABLE", "Server '%s' is not available for media for '%s/%s' :%s", string(rembuf, remote), server, mediaid, e.what() }; } conf::item media_download_timeout { { "name", "ircd.media.download.timeout" }, { "default", 15L }, }; std::pair> download(const mutable_buffer &head_buf, const string_view &server, const string_view &mediaid, net::hostport remote, server::request::opts *const opts) { thread_local char rembuf[256]; assert(remote || !my_host(server)); assert(!remote || !my_host(string(rembuf, remote))); if(!remote) remote = server; window_buffer wb{head_buf}; thread_local char uri[4_KiB]; http::request { wb, host(remote), "GET", fmt::sprintf { uri, "/_matrix/media/r0/download/%s/%s", server, mediaid } }; const const_buffer out_head { wb.completed() }; // Remaining space in buffer is used for received head const mutable_buffer in_head { data(head_buf) + size(out_head), size(head_buf) - size(out_head) }; //TODO: --- This should use the progress callback to build blocks server::request remote_request { remote, { out_head }, { in_head, {} }, opts }; if(!remote_request.wait(seconds(media_download_timeout), std::nothrow)) throw m::error { http::GATEWAY_TIMEOUT, "M_MEDIA_DOWNLOAD_TIMEOUT", "Server '%s' did not respond with media for '%s/%s' in time", string(rembuf, remote), server, mediaid }; const auto &code { remote_request.get() }; if(code != http::OK) return {}; parse::buffer pb{remote_request.in.head}; parse::capstan pc{pb}; pc.read += size(remote_request.in.head); return std::pair> { pc, std::move(remote_request.in.dynamic) }; } size_t write_file(const m::room &room, const m::user::id &user_id, const const_buffer &content, const string_view &content_type) { //TODO: TXN send(room, user_id, "ircd.file.stat", "size", json::members { { "value", long(size(content)) } }); //TODO: TXN send(room, user_id, "ircd.file.stat", "type", json::members { { "value", content_type } }); size_t off{0}, wrote{0}; while(off < size(content)) { const size_t blksz { std::min(size(content) - off, size_t(32_KiB)) }; const const_buffer &block { data(content) + off, blksz }; block_set(room, user_id, block); wrote += size(block); off += blksz; } assert(off == size(content)); assert(wrote == off); return wrote; } size_t read_each_block(const m::room &room, const std::function &closure) { static const m::event::fetch::opts fopts { m::event::keys::include { "content", "type" } }; size_t ret{0}; m::room::messages it { room, 1, &fopts }; // Block buffer const unique_buffer buf { 64_KiB }; for(; bool(it); ++it) { const m::event &event{*it}; if(at<"type"_>(event) != "ircd.file.block") continue; const json::string &hash { at<"content"_>(event).at("hash") }; const auto &blksz { at<"content"_>(event).get("size") }; const const_buffer &block { block_get(buf, hash) }; if(unlikely(size(block) != blksz)) throw error { "File [%s] block [%s] (%s) blksz %zu != %zu", string_view{room.room_id}, string_view{m::get(std::nothrow, it.event_idx(), "event_id", buf)}, hash, blksz, size(block) }; assert(size(block) == blksz); ret += size(block); closure(block); } return ret; } const_buffer block_get(const mutable_buffer &out, const string_view &b58hash) { return read(blocks, b58hash, out); } m::event::id::buf block_set(const m::room &room, const m::user::id &user_id, const const_buffer &block) { static constexpr const auto bufsz { b58encode_size(sha256::digest_size) }; char b58buf[bufsz]; const auto hash { block_set(mutable_buffer{b58buf}, block) }; return send(room, user_id, "ircd.file.block", json::members { { "size", long(size(block)) }, { "hash", hash } }); } string_view block_set(const mutable_buffer &b58buf, const const_buffer &block) { const sha256::buf hash { sha256{block} }; const string_view b58hash { b58encode(b58buf, hash) }; block_set(b58hash, block); return b58hash; } void block_set(const string_view &b58hash, const const_buffer &block) { write(blocks, b58hash, block); } m::room::id::buf file_room_id(const string_view &server, const string_view &file) { m::room::id::buf ret; file_room_id(ret, server, file); return ret; } m::room::id file_room_id(m::room::id::buf &out, const string_view &server, const string_view &file) { if(empty(server) || empty(file)) throw m::BAD_REQUEST { "Invalid MXC: empty server or file parameters..." }; thread_local char buf[512]; const string_view path { fmt::sprintf { buf, "%s/%s", server, file } }; const sha256::buf hash { sha256{path} }; out = { b58encode(buf, hash), my_host() }; return out; }