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 : }
|