LCOV - code coverage report
Current view: top level - xenolith/resources/assets - XLAssetLibrary.cc (source / functions) Hit Total Coverage
Test: coverage.info Lines: 172 307 56.0 %
Date: 2024-05-12 00:16:13 Functions: 34 46 73.9 %

          Line data    Source code
       1             : /**
       2             :  Copyright (c) 2023 Stappler LLC <admin@stappler.dev>
       3             : 
       4             :  Permission is hereby granted, free of charge, to any person obtaining a copy
       5             :  of this software and associated documentation files (the "Software"), to deal
       6             :  in the Software without restriction, including without limitation the rights
       7             :  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
       8             :  copies of the Software, and to permit persons to whom the Software is
       9             :  furnished to do so, subject to the following conditions:
      10             : 
      11             :  The above copyright notice and this permission notice shall be included in
      12             :  all copies or substantial portions of the Software.
      13             : 
      14             :  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
      15             :  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
      16             :  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
      17             :  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
      18             :  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
      19             :  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
      20             :  THE SOFTWARE.
      21             :  **/
      22             : 
      23             : #include "XLAssetLibrary.h"
      24             : #include "XLApplication.h"
      25             : #include "XLAsset.h"
      26             : #include "XLStorageComponent.h"
      27             : #include "XLStorageServer.h"
      28             : #include "XLNetworkController.h"
      29             : #include "SPSqlHandle.h"
      30             : 
      31             : namespace STAPPLER_VERSIONIZED stappler::xenolith::storage {
      32             : 
      33             : class AssetComponent : public Component {
      34             : public:
      35             :         static constexpr auto DtKey = "XL.AssetLibrary.dt";
      36             : 
      37          21 :         virtual ~AssetComponent() { }
      38             : 
      39             :         AssetComponent(AssetComponentContainer *, ComponentLoader &, StringView name);
      40             : 
      41          42 :         const db::Scheme &getAssets() const { return _assets; }
      42          42 :         const db::Scheme &getVersions() const { return _versions; }
      43             : 
      44             :         virtual void handleChildInit(const Server &, const db::Transaction &) override;
      45             : 
      46             :         void cleanup(const db::Transaction &t);
      47             : 
      48             :         db::Value getAsset(const db::Transaction &, StringView) const;
      49             :         db::Value createAsset(const db::Transaction &, StringView, TimeInterval) const;
      50             : 
      51             :         void updateAssetTtl(const db::Transaction &, int64_t, TimeInterval) const;
      52             : 
      53             : protected:
      54             :         AssetComponentContainer *_container = nullptr;
      55             :         db::Scheme _assets = db::Scheme("assets");
      56             :         db::Scheme _versions = db::Scheme("versions");
      57             : };
      58             : 
      59             : XL_DECLARE_EVENT_CLASS(AssetLibrary, onLoaded)
      60             : 
      61          21 : bool AssetComponentContainer::init(StringView name, AssetLibrary *l) {
      62          21 :         if (!ComponentContainer::init(name)) {
      63           0 :                 return false;
      64             :         }
      65             : 
      66          21 :         _library = l;
      67          21 :         return true;
      68             : }
      69             : 
      70          21 : void AssetComponentContainer::handleStorageInit(storage::ComponentLoader &loader) {
      71          21 :         ComponentContainer::handleStorageInit(loader);
      72          21 :         _component = new AssetComponent(this, loader, "AssetComponent");
      73          21 : }
      74             : 
      75          21 : void AssetComponentContainer::handleStorageDisposed(const db::Transaction &t) {
      76          21 :         _component = nullptr;
      77          21 :         ComponentContainer::handleStorageDisposed(t);
      78          21 : }
      79             : 
      80             : 
      81          21 : AssetComponent::AssetComponent(AssetComponentContainer *c, ComponentLoader &loader, StringView name)
      82          21 : : Component(loader, name), _container(c) {
      83             :         using namespace db;
      84             : 
      85          21 :         loader.exportScheme(_assets.define({
      86          21 :                 Field::Integer("mtime", Flags::AutoMTime),
      87          42 :                 Field::Integer("touch", Flags::AutoCTime),
      88          42 :                 Field::Integer("ttl"),
      89          42 :                 Field::Text("local"),
      90          42 :                 Field::Text("url", MaxLength(2_KiB), Transform::Url, Flags::Unique | Flags::Indexed),
      91          42 :                 Field::Set("versions", _versions),
      92          42 :                 Field::Boolean("download", db::Value(false), Flags::Indexed),
      93          42 :                 Field::Data("data"),
      94             :         }));
      95             : 
      96          21 :         loader.exportScheme(_versions.define({
      97          21 :                 Field::Text("etag", MaxLength(2_KiB)),
      98          42 :                 Field::Integer("ctime", Flags::AutoCTime),
      99          42 :                 Field::Integer("mtime", Flags::AutoMTime),
     100          42 :                 Field::Integer("size"),
     101          42 :                 Field::Text("type"),
     102          42 :                 Field::Boolean("complete", db::Value(false)),
     103          42 :                 Field::Object("asset", _assets, RemovePolicy::Cascade),
     104             :         }));
     105          21 : }
     106             : 
     107          21 : void AssetComponent::handleChildInit(const Server &serv, const db::Transaction &t) {
     108          21 :         Component::handleChildInit(serv, t);
     109             : 
     110          21 :         filesystem::mkdir(filesystem::cachesPath<Interface>("assets"));
     111             : 
     112          21 :         Time time = Time::now();
     113          21 :         Vector<Rc<Asset>> assetsVec;
     114             : 
     115          21 :         auto assets = _assets.select(t, db::Query().select("download", db::Value(true)));
     116          21 :         for (auto &it : assets.asArray()) {
     117           0 :                 auto versions = _versions.select(t, db::Query().select("asset", it.getValue("__oid")));
     118           0 :                 it.setValue(move(versions), "versions");
     119             : 
     120           0 :                 auto &asset = assetsVec.emplace_back(Rc<Asset>::alloc(_container->getLibrary(), it));
     121           0 :                 asset->touch(time);
     122             : 
     123           0 :                 _assets.update(t, it, db::Value({
     124           0 :                         pair("touch", db::Value(asset->getTouch().toMicros()))
     125           0 :                 }));
     126           0 :         }
     127             : 
     128          21 :         cleanup(t);
     129             : 
     130          42 :         _container->getLibrary()->getApplication()->performOnMainThread([assetsVec = move(assetsVec), lib = _container->getLibrary()] () mutable {
     131          21 :                 lib->handleLibraryLoaded(move(assetsVec));
     132          21 :         }, _container->getLibrary());
     133          21 : }
     134             : 
     135          21 : void AssetComponent::cleanup(const db::Transaction &t) {
     136          21 :         Time time = Time::now();
     137          21 :         if (auto iface = dynamic_cast<db::sql::SqlHandle *>(t.getAdapter().getBackendInterface())) {
     138          63 :                 iface->performSimpleSelect(toString("SELECT __oid, url FROM ", _assets.getName(),
     139             :                                 " WHERE download == 0 AND ttl != 0 AND (touch + ttl) < ",
     140          42 :                                 time.toMicros(), ";"), [&] (db::Result &res) {
     141          21 :                         for (auto it : res) {
     142           0 :                                 auto path = AssetLibrary::getAssetPath(it.toInteger(0));
     143           0 :                                 filesystem::remove(path, true, true);
     144           0 :                         }
     145          21 :                 });
     146             : 
     147          63 :                 iface->performSimpleQuery(toString("DELETE FROM ", _assets.getName(),
     148             :                                 " WHERE download == 0 AND ttl != 0 AND touch + ttl * 2 < ",
     149          42 :                                 time.toMicros(), ";"));
     150             :         }
     151          21 : }
     152             : 
     153          21 : db::Value AssetComponent::getAsset(const db::Transaction &t, StringView url) const {
     154          21 :         if (auto v = _assets.select(t, db::Query().select("url", db::Value(url))).getValue(0)) {
     155           0 :                 if (auto versions = _versions.select(t, db::Query().select("asset", v.getValue("__oid")))) {
     156           0 :                         v.setValue(move(versions), "versions");
     157           0 :                 }
     158           0 :                 return db::Value(move(v));
     159          21 :         }
     160          21 :         return db::Value();
     161             : }
     162             : 
     163          21 : db::Value AssetComponent::createAsset(const db::Transaction &t, StringView url, TimeInterval ttl) const {
     164         126 :         return _assets.create(t, db::Value({
     165          42 :                 pair("url", db::Value(url)),
     166          42 :                 pair("ttl", db::Value(ttl)),
     167         126 :         }));
     168             : }
     169             : 
     170           0 : void AssetComponent::updateAssetTtl(const db::Transaction &t, int64_t id, TimeInterval ttl) const {
     171           0 :         _assets.update(t, id, db::Value({
     172           0 :                 pair("ttl", db::Value(ttl)),
     173           0 :         }), db::UpdateFlags::NoReturn);
     174           0 : }
     175             : 
     176          21 : String AssetLibrary::getAssetPath(int64_t id) {
     177          42 :         return toString(filesystem::cachesPath<Interface>("assets"), "/", id);
     178             : }
     179             : 
     180          42 : String AssetLibrary::getAssetUrl(StringView url) {
     181          84 :         if (url.starts_with("%") || url.starts_with("app://") || url.starts_with("http://") || url.starts_with("https://")
     182          84 :                          || url.starts_with("ftp://") || url.starts_with("ftps://")) {
     183          42 :                 return url.str<Interface>();
     184           0 :         } else if (url.starts_with("/")) {
     185           0 :                 return filepath::canonical<Interface>(url);
     186             :         } else {
     187           0 :                 return toString("app://", url);
     188             :         }
     189             : }
     190             : 
     191          42 : AssetLibrary::~AssetLibrary() {
     192          21 :         _server = nullptr;
     193          42 : }
     194             : 
     195          21 : bool AssetLibrary::init(Application *app, network::Controller *c, const Value &dbParams) {
     196          21 :         _application = app; // always before server initialization
     197          21 :         _controller = c;
     198          21 :         _container = Rc<AssetComponentContainer>::create("AssetLibrary", this);
     199          21 :         _server = Rc<Server>::create(app, dbParams);
     200          21 :         _server->addComponentContainer(_container);
     201          21 :         return true;
     202             : }
     203             : 
     204          21 : void AssetLibrary::initialize(Application *) {
     205             : 
     206          21 : }
     207             : 
     208          21 : void AssetLibrary::invalidate(Application *app) {
     209          21 :         UpdateTime t;
     210          21 :         update(app, t);
     211          21 :         _liveAssets.clear();
     212          21 :         _assetsByUrl.clear();
     213          21 :         _assetsById.clear();
     214          21 :         _callbacks.clear();
     215             : 
     216          21 :         _server->removeComponentContainer(_container);
     217          21 :         _server = nullptr;
     218          21 : }
     219             : 
     220        3867 : void AssetLibrary::update(Application *, const UpdateTime &t) {
     221        3867 :         auto it = _liveAssets.begin();
     222        3867 :         while (it != _liveAssets.end()) {
     223           0 :                 if ((*it)->isStorageDirty()) {
     224           0 :                         _server->perform([this, value = (*it)->encode(), id = (*it)->getId()] (const Server &, const db::Transaction &t) {
     225           0 :                                 _container->getComponent()->getAssets().update(t, id, db::Value(value), db::UpdateFlags::NoReturn);
     226           0 :                                 return true;
     227             :                         }, this);
     228           0 :                         (*it)->setStorageDirty(false);
     229             :                 }
     230           0 :                 if ((*it)->getReferenceCount() == 1) {
     231           0 :                         it = _liveAssets.erase(it);
     232             :                 } else {
     233           0 :                         ++ it;
     234             :                 }
     235             :         }
     236        3867 : }
     237             : 
     238          42 : bool AssetLibrary::acquireAsset(StringView iurl, AssetCallback &&cb, TimeInterval ttl, Rc<Ref> &&ref) {
     239          42 :         if (!_loaded) {
     240          21 :                 _tmpRequests.push_back(AssetRequest(iurl, move(cb), ttl, move(ref)));
     241          21 :                 return true;
     242             :         }
     243             : 
     244          21 :         auto url = getAssetUrl(iurl);
     245          21 :         if (auto a = getLiveAsset(url)) {
     246           0 :                 if (cb) {
     247           0 :                         cb(a);
     248             :                 }
     249           0 :                 return true;
     250             :         }
     251             : 
     252          21 :         auto it = _callbacks.find(url);
     253          21 :         if (it != _callbacks.end()) {
     254           0 :                 it->second.emplace_back(move(cb), move(ref));
     255             :         } else {
     256          42 :                 _callbacks.emplace(url, Vector<Pair<AssetCallback, Rc<Ref>>>({pair(move(cb), move(ref))}));
     257             : 
     258          21 :                 _server->perform([this, url = move(url), ttl] (const Server &, const db::Transaction &t) {
     259          21 :                         Rc<Asset> asset;
     260          21 :                         if (auto data = _container->getComponent()->getAsset(t, url)) {
     261           0 :                                 if (data.getInteger("ttl") != int64_t(ttl.toMicros())) {
     262           0 :                                         _container->getComponent()->updateAssetTtl(t, data.getInteger("__oid"), ttl);
     263           0 :                                         data.setInteger(ttl.toMicros(), "ttl");
     264             :                                 }
     265             : 
     266           0 :                                 handleAssetLoaded(Rc<Asset>::alloc(this, data));
     267             :                         } else {
     268          21 :                                 if (auto data = _container->getComponent()->createAsset(t, url, ttl)) {
     269          21 :                                         handleAssetLoaded(Rc<Asset>::alloc(this, data));
     270          21 :                                 }
     271          21 :                         }
     272          21 :                         return true;
     273          21 :                 });
     274             :         }
     275             : 
     276          21 :         return true;
     277          21 : }
     278             : 
     279           0 : bool AssetLibrary::acquireAssets(SpanView<AssetRequest> vec, AssetVecCallback &&icb, Rc<Ref> &&ref) {
     280           0 :         if (!_loaded) {
     281           0 :                 if (!icb && !ref) {
     282           0 :                         for (auto &it : vec) {
     283           0 :                                 _tmpRequests.emplace_back(move(it));
     284             :                         }
     285             :                 } else {
     286           0 :                         _tmpMultiRequest.emplace_back(AssetMultiRequest(vec.vec<Interface>(), move(icb), move(ref)));
     287             :                 }
     288           0 :                 return true;
     289             :         }
     290             : 
     291           0 :         size_t assetCount = vec.size();
     292           0 :         auto requests = new Vector<AssetRequest>;
     293             : 
     294           0 :         Vector<Rc<Asset>> *retVec = nullptr;
     295           0 :         AssetVecCallback *cb = nullptr;
     296           0 :         if (cb) {
     297           0 :                 retVec = new Vector<Rc<Asset>>;
     298           0 :                 cb = new AssetVecCallback(move(icb));
     299             :         }
     300             : 
     301           0 :         for (auto &it : vec) {
     302           0 :                 if (auto a = getLiveAsset(it.url)) {
     303           0 :                         if (it.callback) {
     304           0 :                                 it.callback(a);
     305             :                         }
     306           0 :                         if (retVec) {
     307           0 :                                 retVec->emplace_back(a);
     308             :                         }
     309             :                 } else {
     310           0 :                         auto cbit = _callbacks.find(it.url);
     311           0 :                         if (cbit != _callbacks.end()) {
     312           0 :                                 cbit->second.emplace_back(it.callback, ref);
     313           0 :                                 if (cb && retVec) {
     314           0 :                                         cbit->second.emplace_back([cb, retVec, assetCount] (Asset *a) {
     315           0 :                                                 retVec->emplace_back(a);
     316           0 :                                                 if (retVec->size() == assetCount) {
     317           0 :                                                         (*cb)(*retVec);
     318           0 :                                                         delete retVec;
     319           0 :                                                         delete cb;
     320             :                                                 }
     321           0 :                                         }, nullptr);
     322             :                                 }
     323             :                         } else {
     324           0 :                                 Vector<Pair<AssetCallback, Rc<Ref>>> vec;
     325           0 :                                 vec.emplace_back(it.callback, ref);
     326           0 :                                 if (cb && retVec) {
     327           0 :                                         vec.emplace_back([cb, retVec, assetCount] (Asset *a) {
     328           0 :                                                 retVec->emplace_back(a);
     329           0 :                                                 if (retVec->size() == assetCount) {
     330           0 :                                                         (*cb)(*retVec);
     331           0 :                                                         delete retVec;
     332           0 :                                                         delete cb;
     333             :                                                 }
     334           0 :                                         }, nullptr);
     335             :                                 }
     336           0 :                                 _callbacks.emplace(it.url, move(vec));
     337           0 :                                 requests->push_back(it);
     338           0 :                         }
     339             :                 }
     340             :         }
     341             : 
     342           0 :         if (requests->empty()) {
     343           0 :                 if (cb && retVec) {
     344           0 :                         if (retVec->size() == assetCount) {
     345           0 :                                 (*cb)(*retVec);
     346           0 :                                 delete retVec;
     347           0 :                                 delete cb;
     348             :                         }
     349             :                 }
     350           0 :                 delete requests;
     351           0 :                 return true;
     352             :         }
     353             : 
     354           0 :         _server->perform([this, requests] (const Server &, const db::Transaction &t) {
     355           0 :                 db::Vector<int64_t> ids;
     356           0 :                 for (auto &it : *requests) {
     357           0 :                         Rc<Asset> asset;
     358           0 :                         if (auto data = _container->getComponent()->getAsset(t, it.url)) {
     359           0 :                                 if (!db::emplace_ordered(ids, data.getInteger("__oid"))) {
     360           0 :                                         continue;
     361             :                                 }
     362             : 
     363           0 :                                 if (data.getInteger("ttl") != int64_t(it.ttl.toMicros())) {
     364           0 :                                         _container->getComponent()->updateAssetTtl(t, data.getInteger("__oid"), it.ttl);
     365           0 :                                         data.setInteger(it.ttl.toMicros(), "ttl");
     366             :                                 }
     367             : 
     368           0 :                                 handleAssetLoaded(Rc<Asset>::alloc(this, data));
     369             :                         } else {
     370           0 :                                 if (auto data = _container->getComponent()->createAsset(t, it.url, it.ttl)) {
     371           0 :                                         handleAssetLoaded(Rc<Asset>::alloc(this, data));
     372           0 :                                 }
     373           0 :                         }
     374           0 :                 }
     375           0 :                 return true;
     376           0 :         });
     377           0 :         return true;
     378             : }
     379             : 
     380          21 : int64_t AssetLibrary::addVersion(const db::Transaction &t, int64_t assetId, const Asset::VersionData &data) {
     381         147 :         auto version = _container->getComponent()->getVersions().create(t, db::Value({
     382          42 :                 pair("asset", db::Value(assetId)),
     383          42 :                 pair("etag", db::Value(data.etag)),
     384          42 :                 pair("ctime", db::Value(data.ctime)),
     385          42 :                 pair("size", db::Value(data.size)),
     386          42 :                 pair("type", db::Value(data.contentType)),
     387         147 :         }));
     388          42 :         return version.getInteger("__oid");
     389          21 : }
     390             : 
     391           0 : void AssetLibrary::eraseVersion(int64_t id) {
     392           0 :         _server->perform([this, id] (const Server &serv, const db::Transaction &t) {
     393           0 :                 return _container->getComponent()->getVersions().remove(t, id);
     394             :         });
     395           0 : }
     396             : 
     397          42 : void AssetLibrary::setAssetDownload(int64_t id, bool value) {
     398          42 :         _server->perform([this, id, value] (const Server &serv, const db::Transaction &t) -> bool {
     399         210 :                 return _container->getComponent()->getAssets().update(t, id, db::Value({
     400          84 :                         pair("download", db::Value(value))
     401         168 :                 })) ? true : false;
     402             :         });
     403          42 : }
     404             : 
     405          21 : void AssetLibrary::setVersionComplete(int64_t id, bool value) {
     406          21 :         _server->perform([this, id, value] (const Server &serv, const db::Transaction &t) -> bool {
     407         105 :                 return _container->getComponent()->getVersions().update(t, id, db::Value({
     408          42 :                         pair("complete", db::Value(value))
     409          84 :                 })) ? true : false;
     410             :         });
     411          21 : }
     412             : 
     413           0 : void AssetLibrary::cleanup() {
     414           0 :         if (!_controller->isNetworkOnline()) {
     415             :                 // if we had no network connection to restore assets - do not cleanup them
     416           0 :                 return;
     417             :         }
     418             : 
     419           0 :         _server->perform([this] (const storage::Server &serv, const db::Transaction &t) {
     420           0 :                 _container->getComponent()->cleanup(t);
     421           0 :                 return true;
     422             :         }, this);
     423             : }
     424             : 
     425          21 : Asset *AssetLibrary::getLiveAsset(StringView url) const {
     426          21 :         auto it = _assetsByUrl.find(url);
     427          21 :         if (it != _assetsByUrl.end()) {
     428           0 :                 return it->second;
     429             :         }
     430          21 :         return nullptr;
     431             : }
     432             : 
     433           0 : Asset *AssetLibrary::getLiveAsset(int64_t id) const {
     434           0 :         auto it = _assetsById.find(id);
     435           0 :         if (it != _assetsById.end()) {
     436           0 :                 return it->second;
     437             :         }
     438           0 :         return nullptr;
     439             : }
     440             : 
     441          21 : bool AssetLibrary::perform(TaskCallback &&cb, Ref *ref) const {
     442          21 :         return _container->perform(move(cb), ref);
     443             : }
     444             : 
     445          21 : void AssetLibrary::removeAsset(Asset *asset) {
     446          21 :         _assetsById.erase(asset->getId());
     447          21 :         _assetsByUrl.erase(asset->getUrl());
     448          21 : }
     449             : 
     450          21 : void AssetLibrary::handleLibraryLoaded(Vector<Rc<Asset>> &&assets) {
     451          21 :         for (auto &it : assets) {
     452           0 :                 StringView url = it->getUrl();
     453             : 
     454           0 :                 _assetsByUrl.emplace(it->getUrl(), it.get());
     455           0 :                 _assetsById.emplace(it->getId(), it.get());
     456             : 
     457           0 :                 auto iit = _callbacks.find(url);
     458           0 :                 if (iit != _callbacks.end()) {
     459           0 :                         for (auto &cb : iit->second) {
     460           0 :                                 cb.first(it);
     461             :                         }
     462             :                 }
     463             :         }
     464             : 
     465          21 :         assets.clear();
     466             : 
     467          21 :         _loaded = true;
     468             : 
     469          42 :         for (auto &it : _tmpRequests) {
     470          21 :                 acquireAsset(it.url, move(it.callback), it.ttl, move(it.ref));
     471             :         }
     472             : 
     473          21 :         _tmpRequests.clear();
     474             : 
     475          21 :         for (auto &it : _tmpMultiRequest) {
     476           0 :                 acquireAssets(it.vec, move(it.callback), move(it.ref));
     477             :         }
     478             : 
     479          21 :         _tmpMultiRequest.clear();
     480          21 : }
     481             : 
     482          21 : void AssetLibrary::handleAssetLoaded(Rc<Asset> &&asset) {
     483          21 :         _application->performOnMainThread([this, asset = move(asset)] {
     484          21 :                 _assetsById.emplace(asset->getId(), asset);
     485          21 :                 _assetsByUrl.emplace(asset->getUrl(), asset);
     486             : 
     487          21 :                 auto it = _callbacks.find(asset->getUrl());
     488          21 :                 if (it != _callbacks.end()) {
     489          42 :                         for (auto &cb : it->second) {
     490          21 :                                 if (cb.first) {
     491          21 :                                         cb.first(asset);
     492             :                                 }
     493             :                         }
     494          21 :                         _callbacks.erase(it);
     495             :                 }
     496          21 :         }, this);
     497          21 : }
     498             : 
     499             : }

Generated by: LCOV version 1.14