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 "XLAsset.h"
24 : #include "XLAssetLibrary.h"
25 : #include "XLNetworkRequest.h"
26 : #include "XLApplication.h"
27 :
28 : #include "curl/curl.h"
29 :
30 : #if WIN32
31 : #undef interface
32 : #endif
33 :
34 : namespace STAPPLER_VERSIONIZED stappler::xenolith::storage {
35 :
36 0 : AssetLock::~AssetLock() {
37 0 : if (_releaseFunction) {
38 0 : _releaseFunction(_lockedVersion);
39 : }
40 0 : _asset = nullptr;
41 0 : }
42 :
43 0 : StringView AssetLock::getCachePath() const {
44 0 : return _asset->getCachePath();
45 : }
46 :
47 0 : AssetLock::AssetLock(Rc<Asset> &&asset, const AssetVersionData &data, Function<void(const AssetVersionData &)> &&cb, Ref *owner)
48 0 : : _lockedVersion(data), _releaseFunction(move(cb)), _asset(move(asset)), _owner(owner) { }
49 :
50 21 : Asset::Asset(AssetLibrary *lib, const db::Value &val) : _library(lib) {
51 21 : bool resumeDownload = false;
52 21 : const db::Value *versions = nullptr;
53 147 : for (auto &it : val.asDict()) {
54 126 : if (it.first == "__oid") {
55 21 : _id = reinterpretValue<uint64_t>(it.second.getInteger());
56 105 : } else if (it.first == "url") {
57 21 : _url = StringView(it.second.getString()).str<Interface>();
58 84 : } else if (it.first == "data") {
59 0 : _data = Value(it.second);
60 84 : } else if (it.first == "mtime") {
61 21 : _mtime = Time(it.second.getInteger());
62 63 : } else if (it.first == "touch") {
63 21 : _touch = Time(it.second.getInteger());
64 42 : } else if (it.first == "ttl") {
65 21 : _ttl = TimeInterval(it.second.getInteger());
66 21 : } else if (it.first == "download") {
67 21 : resumeDownload = it.second.getBool();
68 0 : } else if (it.first == "versions") {
69 0 : versions = &it.second;
70 : }
71 : }
72 :
73 21 : _path = AssetLibrary::getAssetPath(_id);
74 21 : _cache = toString(_path, "/cache");
75 :
76 21 : filesystem::mkdir(_path);
77 21 : filesystem::mkdir(_cache);
78 :
79 21 : if (versions) {
80 0 : parseVersions(*versions);
81 : }
82 :
83 21 : if (resumeDownload) {
84 0 : download();
85 : }
86 21 : }
87 :
88 42 : Asset::~Asset() {
89 21 : _library->removeAsset(this);
90 42 : }
91 :
92 0 : Rc<AssetLock> Asset::lockVersion(int64_t id, Ref *owner) {
93 0 : std::unique_lock ctx(_mutex);
94 0 : for (auto &it : _versions) {
95 0 : if (it.id == id && it.complete) {
96 0 : ++ it.locked;
97 0 : auto ret = new AssetLock(this, it, [this] (const VersionData &data) {
98 0 : releaseLock(data);
99 0 : }, owner);
100 0 : auto ref = Rc<AssetLock>(ret);
101 0 : ret->release(0);
102 0 : return ref;
103 0 : }
104 : }
105 0 : return nullptr;
106 0 : }
107 :
108 0 : Rc<AssetLock> Asset::lockReadableVersion(Ref *owner) {
109 0 : std::unique_lock ctx(_mutex);
110 0 : for (auto &it : _versions) {
111 0 : if (it.complete && filesystem::exists(it.path)) {
112 0 : ++ it.locked;
113 0 : auto ret = new AssetLock(this, it, [this] (const VersionData &data) {
114 0 : releaseLock(data);
115 0 : }, owner);
116 0 : auto ref = Rc<AssetLock>(ret);
117 0 : ret->release(0);
118 0 : return ref;
119 0 : }
120 : }
121 0 : return nullptr;
122 0 : }
123 :
124 0 : StringView Asset::getContentType() const {
125 0 : std::unique_lock ctx(_mutex);
126 0 : if (auto ver = getReadableVersion()) {
127 0 : return ver->contentType;
128 : } else {
129 0 : for (auto &it : _versions) {
130 0 : if (!it.contentType.empty()) {
131 0 : return it.contentType;
132 : }
133 : }
134 : }
135 0 : return StringView();
136 0 : }
137 :
138 21 : bool Asset::download() {
139 21 : std::unique_lock ctx(_mutex);
140 21 : if (_download) {
141 0 : return true;
142 : }
143 :
144 : do {
145 21 : auto it = _versions.begin();
146 21 : while (it != _versions.end()) {
147 0 : if (it->complete && !filesystem::exists(it->path)) {
148 0 : dropVersion(*it);
149 0 : it = _versions.erase(it);
150 : } else {
151 0 : ++ it;
152 : }
153 : }
154 : } while (0);
155 :
156 21 : auto it = _versions.begin();
157 21 : while (it != _versions.end()) {
158 0 : if (!it->complete) {
159 0 : if (resumeDownload(*it)) {
160 0 : return true;
161 : } else {
162 0 : dropVersion(*it);
163 0 : it = _versions.erase(it);
164 : }
165 : } else {
166 0 : ++ it;
167 : }
168 : }
169 :
170 21 : if (!_versions.empty()) {
171 0 : if (filesystem::exists(_versions.front().path)) {
172 0 : return startNewDownload(_versions.front().ctime, _versions.front().etag);
173 : }
174 : }
175 :
176 21 : return startNewDownload(Time(), StringView());
177 21 : }
178 :
179 0 : void Asset::touch(Time t) {
180 0 : std::unique_lock ctx(_mutex);
181 0 : _touch = t;
182 0 : _dirty = true;
183 0 : }
184 :
185 0 : void Asset::clear() {
186 0 : std::unique_lock ctx(_mutex);
187 0 : auto it = _versions.begin();
188 0 : while (it != _versions.end()) {
189 0 : if (it->complete) {
190 0 : dropVersion(*it);
191 0 : it = _versions.erase(it);
192 : } else {
193 0 : ++ it;
194 : }
195 : }
196 0 : setDirty(Flags(CacheDataUpdated | DownloadFailed));
197 0 : }
198 :
199 0 : bool Asset::isDownloadAvailable() const {
200 0 : std::unique_lock ctx(_mutex);
201 0 : return _versions.empty() || (!_versions.empty() && !_versions.back().complete);
202 0 : }
203 :
204 0 : bool Asset::isDownloadInProgress() const {
205 0 : std::unique_lock ctx(_mutex);
206 0 : return _download;
207 0 : }
208 :
209 0 : float Asset::getProgress() const {
210 0 : std::unique_lock ctx(_mutex);
211 0 : for (auto &it : _versions) {
212 0 : if (it.id == _downloadId) {
213 0 : return it.progress;
214 : }
215 : }
216 0 : return _versions.empty() ? 0.0f : (_versions.front().complete ? 1.0f : 0.0f);
217 0 : }
218 :
219 0 : int64_t Asset::getReadableVersionId() const {
220 0 : std::unique_lock ctx(_mutex);
221 0 : if (auto v = getReadableVersion()) {
222 0 : return v->id;
223 : }
224 0 : return 0;
225 0 : }
226 :
227 0 : void Asset::setData(const Value &d) {
228 0 : std::unique_lock ctx(_mutex);
229 0 : _data = d;
230 0 : _dirty = true;
231 0 : }
232 :
233 0 : void Asset::setData(Value &&d) {
234 0 : std::unique_lock ctx(_mutex);
235 0 : _data = std::move(d);
236 0 : _dirty = true;
237 0 : }
238 :
239 0 : Value Asset::encode() const {
240 0 : std::unique_lock ctx(_mutex);
241 : return Value({
242 0 : pair("ttl", Value(_ttl.toMicros())),
243 0 : pair("touch", Value(_touch.toMicros())),
244 0 : pair("data", Value(_data)),
245 0 : });
246 0 : }
247 :
248 0 : const AssetVersionData * Asset::getReadableVersion() const {
249 0 : for (auto &it : _versions) {
250 0 : if (it.complete && filesystem::exists(it.path)) {
251 0 : return ⁢
252 : }
253 : }
254 0 : return nullptr;
255 : }
256 :
257 0 : void Asset::parseVersions(const db::Value &downloads) {
258 0 : std::unique_lock ctx(_mutex);
259 :
260 0 : Set<String> paths;
261 0 : Set<String> pathsToRemove;
262 :
263 0 : for (auto &download : downloads.asArray()) {
264 0 : VersionData data;
265 0 : for (auto &it : download.asDict()) {
266 0 : if (it.first == "__oid") {
267 0 : data.id = it.second.getInteger();
268 0 : } else if (it.first == "etag") {
269 0 : data.etag = StringView(it.second.getString()).str<Interface>();
270 0 : } else if (it.first == "ctime") {
271 0 : data.ctime = Time(it.second.getInteger());
272 0 : } else if (it.first == "mtime") {
273 0 : data.mtime = Time(it.second.getInteger());
274 0 : } else if (it.first == "size") {
275 0 : data.size = it.second.getInteger();
276 0 : } else if (it.first == "type") {
277 0 : data.contentType = StringView(it.second.getString()).str<Interface>();
278 0 : } else if (it.first == "complete") {
279 0 : data.complete = it.second.getBool();
280 : }
281 : }
282 :
283 0 : auto tag = StringView(data.etag);
284 0 : tag.trimChars<StringView::Chars<'"', '\'', ' ', '-'>>();
285 0 : auto versionPath = toString(_path, "/", data.ctime.toMicros(), "-", tag);
286 0 : auto iit = paths.find(versionPath);
287 0 : if (iit != paths.end()) {
288 0 : _library->eraseVersion(data.id);
289 : } else {
290 0 : if (filesystem::exists(versionPath)) {
291 0 : auto &v = _versions.emplace_back(move(data));
292 0 : v.path = move(versionPath);
293 0 : v.download = true;
294 :
295 0 : paths.emplace(v.path);
296 : } else {
297 0 : _library->eraseVersion(data.id);
298 : }
299 : }
300 0 : }
301 :
302 0 : filesystem::ftw(_path, [&, this] (StringView path, bool isFile) {
303 0 : if (!isFile && path != _cache && path != _path) {
304 0 : auto it = paths.find(path);
305 0 : if (it == paths.end()) {
306 0 : pathsToRemove.emplace(path.str<Interface>());
307 : }
308 : }
309 0 : }, 1);
310 :
311 0 : for (auto &it : pathsToRemove) {
312 0 : filesystem::remove(it, true, true);
313 : }
314 :
315 0 : bool localFound = false;
316 0 : bool pendingFound = false;
317 :
318 0 : auto it = _versions.begin();
319 0 : while (it != _versions.end()) {
320 0 : if (it->complete) {
321 0 : if (!localFound) {
322 0 : localFound = true;
323 0 : ++ it;
324 : } else {
325 0 : _library->eraseVersion(it->id);
326 0 : it = _versions.erase(it);
327 : }
328 : } else {
329 0 : if (!pendingFound) {
330 0 : pendingFound = true;
331 0 : ++ it;
332 : } else {
333 0 : _library->eraseVersion(it->id);
334 0 : it = _versions.erase(it);
335 : }
336 : }
337 : }
338 0 : }
339 :
340 : struct AssetDownloadData : Ref {
341 : Rc<Asset> asset;
342 : Asset::VersionData data;
343 : FILE *inputFile = nullptr;
344 : bool valid = true;
345 : float progress = 0.0f;
346 :
347 21 : AssetDownloadData(Rc<Asset> &&a)
348 21 : : asset(move(a)) { }
349 :
350 0 : AssetDownloadData(Rc<Asset> &&a, Asset::VersionData &data)
351 0 : : asset(move(a)), data(data) { }
352 : };
353 :
354 21 : bool Asset::startNewDownload(Time ctime, StringView etag) {
355 21 : auto data = Rc<AssetDownloadData>::alloc(this);
356 :
357 42 : auto req = Rc<network::Request>::create([&, this] (network::Handle &handle) {
358 21 : handle.init(network::Method::Get, _url);
359 :
360 21 : handle.setMTime(ctime.toMicros());
361 21 : handle.setETag(etag);
362 :
363 21 : handle.setHeaderCallback([data = data.get()] (StringView key, StringView value) {
364 210 : if (key == "last-modified") {
365 21 : data->data.ctime = std::max(Time::fromHttp(value), data->data.ctime);
366 189 : } else if (key == "x-filemodificationtime") {
367 21 : if (uint64_t v = value.readInteger(10).get(0)) {
368 21 : data->data.ctime = std::max(Time::microseconds(v), data->data.ctime);
369 : }
370 168 : } else if (key == "etag") {
371 21 : data->data.etag = value.str<Interface>();
372 147 : } else if (key == "content-length") {
373 21 : data->data.size = std::max(size_t(value.readInteger(10).get(0)), data->data.size);
374 126 : } else if (key == "x-filesize") {
375 21 : data->data.size = std::max(size_t(value.readInteger(10).get(0)), data->data.size);
376 105 : } else if (key == "content-type") {
377 21 : data->data.contentType = value.str<Interface>();
378 : }
379 210 : });
380 21 : handle.setReceiveCallback([this, data = data.get()] (char *bytes, size_t size) {
381 147 : if (!data->valid) {
382 0 : return size_t(CURL_WRITEFUNC_ERROR);
383 : }
384 :
385 147 : if (!data->inputFile) {
386 21 : auto tag = StringView(data->data.etag);
387 21 : tag.trimChars<StringView::Chars<'"', '\'', ' ', '-'>>();
388 21 : data->data.path = toString(_path, "/", data->data.ctime.toMicros(), "-", tag);
389 21 : data->inputFile = filesystem::native::fopen_fn(data->data.path.data(), "w");
390 21 : if (!data->inputFile) {
391 0 : return size_t(CURL_WRITEFUNC_ERROR);
392 : }
393 21 : addVersion(data);
394 : }
395 :
396 147 : return size_t(fwrite(bytes, size, 1, data->inputFile) * size);
397 : });
398 21 : return true;
399 21 : }, data);
400 :
401 21 : req->setDownloadProgress([this, data = data.get()] (const network::Request &, int64_t total, int64_t now) {
402 21 : data->progress = float(now) / float(total);
403 21 : setDownloadProgress(data->data.id, data->progress);
404 21 : });
405 :
406 21 : _download = true;
407 21 : _library->setAssetDownload(_id, _download);
408 :
409 21 : req->perform(_library->getController(), [this, data = data.get()] (const network::Request &req, bool success) {
410 21 : if (data->inputFile) {
411 21 : fclose(data->inputFile);
412 21 : data->inputFile = nullptr;
413 :
414 21 : setDownloadComplete(data->data, data->valid && success);
415 21 : return;
416 : } else {
417 0 : auto code = req.getHandle().getResponseCode();
418 0 : if (code >= 300 && code < 400) {
419 0 : setFileValidated(success);
420 0 : return;
421 : }
422 : }
423 :
424 0 : setDownloadComplete(data->data, data->valid && false);
425 : });
426 21 : return true;
427 21 : }
428 :
429 0 : bool Asset::resumeDownload(VersionData &d) {
430 0 : filesystem::Stat stat;
431 0 : if (!filesystem::stat(d.path, stat)) {
432 0 : return false;
433 : }
434 :
435 0 : auto data = Rc<AssetDownloadData>::alloc(this, d);
436 :
437 0 : auto req = Rc<network::Request>::create([&, this] (network::Handle &handle) {
438 0 : handle.init(network::Method::Get, _url);
439 :
440 0 : handle.setResumeOffset(stat.size);
441 0 : handle.setHeaderCallback([data = data.get()] (StringView key, StringView value) {
442 0 : if (key == "last-modified") {
443 0 : if (Time::fromHttp(value) > data->data.ctime) {
444 0 : data->valid = false;
445 : }
446 0 : } else if (key == "etag") {
447 0 : if (data->data.etag != value) {
448 0 : data->valid = false;
449 : }
450 : }
451 0 : });
452 0 : handle.setReceiveCallback([data = data.get()] (char *bytes, size_t size) {
453 0 : if (!data->valid) {
454 0 : return size_t(CURL_WRITEFUNC_ERROR);
455 : }
456 :
457 0 : if (!data->inputFile) {
458 0 : data->inputFile = filesystem::native::fopen_fn(data->data.path.data(), "a");
459 0 : if (!data->inputFile) {
460 0 : return size_t(CURL_WRITEFUNC_ERROR);
461 : }
462 : }
463 :
464 0 : return size_t(fwrite(bytes, size, 1, data->inputFile) * size);
465 : });
466 0 : return true;
467 0 : }, data);
468 :
469 0 : req->setDownloadProgress([this, data = data.get()] (const network::Request &, int64_t total, int64_t now) {
470 0 : data->progress = float(now) / float(total);
471 0 : setDownloadProgress(data->data.id, data->progress);
472 0 : });
473 :
474 0 : _downloadId = d.id;
475 0 : _download = true;
476 0 : _library->setAssetDownload(_id, _download);
477 :
478 0 : req->perform(_library->getController(), [this, data = data.get()] (const network::Request &req, bool success) {
479 0 : if (data->inputFile) {
480 0 : fclose(data->inputFile);
481 0 : data->inputFile = nullptr;
482 : }
483 :
484 0 : setDownloadComplete(data->data, data->valid && success);
485 0 : });
486 0 : return true;
487 0 : }
488 :
489 21 : void Asset::setDownloadProgress(int64_t id, float progress) {
490 21 : std::unique_lock ctx(_mutex);
491 21 : for (auto &it : _versions) {
492 0 : if (it.id == id) {
493 0 : it.progress = progress;
494 0 : setDirty(Flags(Update::DownloadProgress));
495 : }
496 : }
497 21 : }
498 :
499 21 : void Asset::setDownloadComplete(VersionData &data, bool success) {
500 21 : std::unique_lock ctx(_mutex);
501 21 : data.complete = success;
502 :
503 21 : _download = false;
504 21 : _library->setAssetDownload(_id, _download);
505 :
506 21 : if (success) {
507 21 : for (auto &it : _versions) {
508 21 : if (it.id == data.id) {
509 21 : replaceVersion(data);
510 21 : setDirty(Flags(Update::DownloadCompleted | Update::DownloadSuccessful | Update::CacheDataUpdated));
511 21 : _library->setVersionComplete(data.id, true);
512 21 : return;
513 : }
514 : }
515 : } else {
516 0 : auto it = _versions.begin();
517 0 : while (it != _versions.end()) {
518 0 : if (it->id == data.id) {
519 0 : dropVersion(*it);
520 0 : it = _versions.erase(it);
521 0 : setDirty(Flags(Update::DownloadCompleted | Update::DownloadFailed));
522 : } else {
523 0 : ++ it;
524 : }
525 : }
526 : }
527 :
528 0 : _downloadId = 0;
529 21 : }
530 :
531 0 : void Asset::setFileValidated(bool success) {
532 0 : std::unique_lock ctx(_mutex);
533 0 : _download = false;
534 0 : _library->setAssetDownload(_id, _download);
535 0 : _downloadId = 0;
536 :
537 0 : setDirty(Flags(CacheDataUpdated));
538 0 : }
539 :
540 21 : void Asset::replaceVersion(VersionData &data) {
541 42 : for (auto &it : _versions) {
542 21 : if (it.id != data.id) {
543 0 : dropVersion(it);
544 : }
545 : }
546 :
547 21 : _versions.clear();
548 21 : _versions.emplace_back(data);
549 21 : _touch = Time::now();
550 21 : }
551 :
552 21 : void Asset::addVersion(AssetDownloadData *data) {
553 21 : _library->perform([this, data] (const Server &, const db::Transaction &t) {
554 21 : auto id = _library->addVersion(t, _id, data->data);
555 21 : _library->getApplication()->performOnMainThread([this, id, data] {
556 21 : std::unique_lock ctx(_mutex);
557 21 : _downloadId = data->data.id = id;
558 21 : _versions.emplace_back(data->data);
559 21 : setDirty(Flags(Update::DownloadStarted));
560 21 : }, data);
561 21 : return true;
562 : }, data);
563 21 : }
564 :
565 0 : void Asset::dropVersion(const VersionData &data) {
566 0 : if (!data.locked) {
567 0 : filesystem::remove(data.path, true, true);
568 : }
569 0 : _library->eraseVersion(data.id);
570 0 : }
571 :
572 0 : void Asset::releaseLock(const VersionData &data) {
573 0 : std::unique_lock ctx(_mutex);
574 0 : for (auto &it : _versions) {
575 0 : if (it.id == data.id) {
576 0 : -- it.locked;
577 0 : return;
578 : }
579 : }
580 :
581 0 : filesystem::remove(data.path, true, true);
582 0 : }
583 :
584 : }
|