source: appstream-generator/src/asgen/datastore.d @ 4841

Last change on this file since 4841 was 4841, checked in by Juanma, 2 years ago

Initial release

File size: 21.5 KB
Line 
1/*
2 * Copyright (C) 2016 Matthias Klumpp <matthias@tenstral.net>
3 *
4 * Licensed under the GNU Lesser General Public License Version 3
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License as published by
8 * the Free Software Foundation, either version 3 of the license, or
9 * (at your option) any later version.
10 *
11 * This software is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 * GNU Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this software.  If not, see <http://www.gnu.org/licenses/>.
18 */
19
20module asgen.datastore;
21
22import std.stdio;
23import std.string;
24import std.conv : to, octal;
25import std.file : mkdirRecurse;
26import std.path : buildPath, buildNormalizedPath, pathSplitter;
27import std.array : appender;
28import std.typecons : Tuple, scoped;
29import std.json;
30static import std.math;
31
32import asgen.bindings.lmdb;
33import appstream.Metadata;
34import appstream.Component;
35
36import asgen.config;
37import asgen.logging;
38import asgen.config : DataType;
39import asgen.result;
40
41
42/**
43 * Main database containing information about scanned packages,
44 * the components they provide, the component metadata itself,
45 * issues found as well as statistics about the metadata evolution
46 * over time.
47 **/
48final class DataStore
49{
50
51private:
52    MDB_envp dbEnv;
53    MDB_dbi dbRepoInfo;
54    MDB_dbi dbPackages;
55    MDB_dbi dbDataXml;
56    MDB_dbi dbDataYaml;
57    MDB_dbi dbHints;
58    MDB_dbi dbStats;
59
60    bool opened;
61    Metadata mdata;
62
63    string mediaDir;
64
65public:
66
67    struct StatisticsEntry
68    {
69        size_t time;
70        JSONValue data;
71    }
72
73    this ()
74    {
75        opened = false;
76        mdata = new Metadata ();
77        mdata.setLocale ("ALL");
78        mdata.setFormatVersion (Config.get ().formatVersion);
79        mdata.setWriteHeader(false);
80    }
81
82    ~this ()
83    {
84        if (opened)
85            dbEnv.mdb_env_close ();
86    }
87
88    @property
89    string mediaExportPoolDir ()
90    {
91        return mediaDir;
92    }
93
94    private void checkError (int rc, string msg)
95    {
96        if (rc != 0) {
97            import std.format;
98            throw new Exception (format ("%s[%s]: %s", msg, rc, mdb_strerror (rc).fromStringz));
99        }
100    }
101
102    private void printVersionDbg ()
103    {
104        import std.stdio : writeln;
105        int major, minor, patch;
106        auto ver = mdb_version (&major, &minor, &patch);
107        logDebug ("Using %s major=%s minor=%s patch=%s", ver.fromStringz, major, minor, patch);
108    }
109
110    void open (string dir, string mediaBaseDir)
111    {
112        int rc;
113        assert (opened == false);
114
115        // add LMDB version we are using to the debug output
116        printVersionDbg ();
117
118        // ensure the cache directory exists
119        mkdirRecurse (dir);
120
121        rc = mdb_env_create (&dbEnv);
122        scope (success) opened = true;
123        scope (failure) dbEnv.mdb_env_close ();
124        checkError (rc, "mdb_env_create");
125
126        // We are going to use at max 6 sub-databases:
127        // packages, hints, metadata_xml, metadata_yaml, statistics
128        rc = dbEnv.mdb_env_set_maxdbs (6);
129        checkError (rc, "mdb_env_set_maxdbs");
130
131        // set a huge map size to be futureproof.
132        // This means we're cruel to non-64bit users, but this
133        // software is supposed to be run on 64bit machines anyway.
134        auto mapsize = cast (size_t) std.math.pow (512L, 4);
135        rc = dbEnv.mdb_env_set_mapsize (mapsize);
136        checkError (rc, "mdb_env_set_mapsize");
137
138        // open database
139        rc = dbEnv.mdb_env_open (dir.toStringz (),
140                                 MDB_NOMETASYNC | MDB_NOTLS,
141                                 octal!755);
142        checkError (rc, "mdb_env_open");
143
144        // open sub-databases in the environment
145        MDB_txnp txn;
146        rc = dbEnv.mdb_txn_begin (null, 0, &txn);
147        checkError (rc, "mdb_txn_begin");
148        scope (failure) txn.mdb_txn_abort ();
149
150        rc = txn.mdb_dbi_open ("packages", MDB_CREATE, &dbPackages);
151        checkError (rc, "open packages database");
152
153        rc = txn.mdb_dbi_open ("repository", MDB_CREATE, &dbRepoInfo);
154        checkError (rc, "open repository database");
155
156        rc = txn.mdb_dbi_open ("metadata_xml", MDB_CREATE, &dbDataXml);
157        checkError (rc, "open metadata (xml) database");
158
159        rc = txn.mdb_dbi_open ("metadata_yaml", MDB_CREATE, &dbDataYaml);
160        checkError (rc, "open metadata (yaml) database");
161
162        rc = txn.mdb_dbi_open ("hints", MDB_CREATE, &dbHints);
163        checkError (rc, "open hints database");
164
165        rc = txn.mdb_dbi_open ("statistics", MDB_CREATE | MDB_INTEGERKEY, &dbStats);
166        checkError (rc, "open statistics database");
167
168        rc = txn.mdb_txn_commit ();
169        checkError (rc, "mdb_txn_commit");
170
171        this.mediaDir = buildPath (mediaBaseDir, "pool");
172        mkdirRecurse (this.mediaDir);
173    }
174
175    void open (Config conf)
176    {
177        this.open (buildPath (conf.databaseDir, "main"), conf.mediaExportDir);
178    }
179
180    private MDB_val makeDbValue (string data)
181    {
182        import core.stdc.string : strlen;
183        MDB_val mval;
184        auto cdata = data.toStringz ();
185        mval.mv_size = char.sizeof * strlen (cdata) + 1;
186        mval.mv_data = cast(void *) cdata;
187        return mval;
188    }
189
190    private MDB_txnp newTransaction (uint flags = 0)
191    {
192        int rc;
193        MDB_txnp txn;
194
195        rc = dbEnv.mdb_txn_begin (null, flags, &txn);
196        checkError (rc, "mdb_txn_begin");
197
198        return txn;
199    }
200
201    private void commitTransaction (MDB_txnp txn)
202    {
203        auto rc = txn.mdb_txn_commit ();
204        checkError (rc, "mdb_txn_commit");
205    }
206
207    private void quitTransaction (MDB_txnp txn)
208    {
209        if (txn is null)
210            return;
211        txn.mdb_txn_abort ();
212    }
213
214    private void putKeyValue (MDB_dbi dbi, string key, string value)
215    {
216        MDB_val dbkey, dbvalue;
217
218        dbkey = makeDbValue (key);
219        dbvalue = makeDbValue (value);
220
221        auto txn = newTransaction ();
222        scope (success) commitTransaction (txn);
223        scope (failure) quitTransaction (txn);
224
225        auto res = txn.mdb_put (dbi, &dbkey, &dbvalue, 0);
226        checkError (res, "mdb_put");
227    }
228
229    private string getValue (MDB_dbi dbi, MDB_val dkey)
230    {
231        import std.conv;
232        MDB_val dval;
233        MDB_cursorp cur;
234
235        auto txn = newTransaction (MDB_RDONLY);
236        scope (exit) quitTransaction (txn);
237
238        auto res = txn.mdb_cursor_open (dbi, &cur);
239        scope (exit) cur.mdb_cursor_close ();
240        checkError (res, "mdb_cursor_open");
241
242        res = cur.mdb_cursor_get (&dkey, &dval, MDB_SET);
243        if (res == MDB_NOTFOUND)
244            return null;
245        checkError (res, "mdb_cursor_get");
246
247        auto data = fromStringz (cast(char*) dval.mv_data);
248        return to!string (data);
249    }
250
251    private string getValue (MDB_dbi dbi, string key)
252    {
253        MDB_val dkey;
254        dkey = makeDbValue (key);
255
256        return getValue (dbi, dkey);
257    }
258
259    bool metadataExists (DataType dtype, string gcid)
260    {
261        return getMetadata (dtype, gcid) !is null;
262    }
263
264    void setMetadata (DataType dtype, string gcid, string asdata)
265    {
266        if (dtype == DataType.XML)
267            putKeyValue (dbDataXml, gcid, asdata);
268        else
269            putKeyValue (dbDataYaml, gcid, asdata);
270    }
271
272    string getMetadata (DataType dtype, string gcid)
273    {
274        string data;
275        if (dtype == DataType.XML)
276            data = getValue (dbDataXml, gcid);
277        else
278            data = getValue (dbDataYaml, gcid);
279        return data;
280    }
281
282    bool hasHints (string pkid)
283    {
284        return getValue (dbHints, pkid) !is null;
285    }
286
287    void setHints (string pkid, string hintsYaml)
288    {
289        putKeyValue (dbHints, pkid, hintsYaml);
290    }
291
292    string getHints (string pkid)
293    {
294        return getValue (dbHints, pkid);
295    }
296
297    string getPackageValue (string pkid)
298    {
299        return getValue (dbPackages, pkid);
300    }
301
302    void setPackageIgnore (string pkid)
303    {
304        putKeyValue (dbPackages, pkid, "ignore");
305    }
306
307    bool isIgnored (string pkid)
308    {
309        auto val = getValue (dbPackages, pkid);
310        return val == "ignore";
311    }
312
313    bool packageExists (string pkid)
314    {
315        auto val = getValue (dbPackages, pkid);
316        return val !is null;
317    }
318
319    void addGeneratorResult (DataType dtype, GeneratorResult gres)
320    {
321        // if the package has no components or hints,
322        // mark it as always-ignore
323        if (gres.packageIsIgnored ()) {
324            setPackageIgnore (gres.pkid);
325            return;
326        }
327
328        foreach (ref cpt; gres.getComponents ()) {
329            auto gcid = gres.gcidForComponent (cpt);
330            if (metadataExists (dtype, gcid)) {
331                // we already have seen this exact metadata - only adjust the reference,
332                // and don't regenerate it.
333                continue;
334            }
335
336            mdata.clearComponents ();
337            mdata.addComponent (cpt);
338
339            // convert out compoent into metadata
340            string data;
341            try {
342                if (dtype == DataType.XML) {
343                    data = mdata.componentsToCollection (FormatKind.XML);
344                } else {
345                    data = mdata.componentsToCollection (FormatKind.YAML);
346                }
347            } catch (Exception e) {
348                gres.addHint (cpt.getId (), "metadata-serialization-failed", e.msg);
349                continue;
350            }
351            // remove trailing whitespaces and linebreaks
352            data = data.stripRight ();
353
354            // store metadata
355            if (!empty (data))
356                setMetadata (dtype, gcid, data);
357        }
358
359        if (gres.hintsCount () > 0) {
360            auto hintsJson = gres.hintsToJson ();
361            if (!hintsJson.empty)
362                setHints (gres.pkid, hintsJson);
363        }
364
365        auto gcids = gres.getGCIDs ();
366        if (gcids.empty) {
367            // no global components, and we're not ignoring this component.
368            // this means we likely have hints stored for this one. Mark it
369            // as "seen" so we don't reprocess it again.
370            putKeyValue (dbPackages, gres.pkid, "seen");
371        } else {
372            import std.array : join;
373            // store global component IDs for this package as newline-separated list
374            auto gcidVal = join (gcids, "\n");
375
376            putKeyValue (dbPackages, gres.pkid, gcidVal);
377        }
378    }
379
380    string[] getGCIDsForPackage (string pkid)
381    {
382        auto pkval = getPackageValue (pkid);
383        if (pkval == "ignore")
384            return null;
385        if (pkval == "seen")
386            return null;
387
388        auto validCids = appender!(string[]);
389        auto cids = pkval.split ("\n");
390        foreach (cid; cids) {
391            if (cid.empty)
392                continue;
393            validCids ~= cid;
394        }
395
396        return validCids.data;
397    }
398
399    string[] getMetadataForPackage (DataType dtype, string pkid)
400    {
401        auto gcids = getGCIDsForPackage (pkid);
402        if (gcids is null)
403            return null;
404
405        auto res = appender!(string[]);
406        foreach (cid; gcids) {
407            auto data = getMetadata (dtype, cid);
408            if (!data.empty)
409                res ~= data;
410        }
411
412        return res.data;
413    }
414
415    /**
416     * Drop a package from the database. This process might leave cruft behind,
417     * which can be collected using the cleanupCruft() method.
418     */
419    void removePackage (string pkid)
420    {
421        MDB_val dbkey;
422
423        dbkey = makeDbValue (pkid);
424
425        auto txn = newTransaction ();
426        scope (success) commitTransaction (txn);
427        scope (failure) quitTransaction (txn);
428
429        auto res = txn.mdb_del (dbPackages, &dbkey, null);
430        if (res != MDB_NOTFOUND)
431            checkError (res, "mdb_del");
432
433        res = txn.mdb_del (dbHints, &dbkey, null);
434        if (res != MDB_NOTFOUND)
435            checkError (res, "mdb_del");
436    }
437
438    private auto getActiveGCIDs ()
439    {
440        MDB_val dkey, dval;
441        MDB_cursorp cur;
442        string[long] stats;
443
444        auto txn = newTransaction (MDB_RDONLY);
445        scope (exit) quitTransaction (txn);
446
447        auto res = txn.mdb_cursor_open (dbPackages, &cur);
448        scope (exit) cur.mdb_cursor_close ();
449        checkError (res, "mdb_cursor_open (gcids)");
450
451        bool[string] gcids;
452        while (cur.mdb_cursor_get (&dkey, &dval, MDB_NEXT) == 0) {
453            auto pkval = to!string (fromStringz (cast(char*) dval.mv_data));
454            if ((pkval == "ignore") || (pkval == "seen"))
455                continue;
456
457            foreach (gcid; pkval.split ("\n"))
458                gcids[gcid] = true;
459        }
460
461        return gcids;
462    }
463
464    void cleanupCruft ()
465    {
466        import std.file;
467        import std.array : array;
468
469        if (mediaDir is null) {
470            logError ("Can not clean up cruft: No media directory is set.");
471            return;
472        }
473
474        auto activeGCIDs = getActiveGCIDs ();
475        bool gcidReferenced (string gcid)
476        {
477            // we use an associative array as a set here
478            return (gcid in activeGCIDs) !is null;
479        }
480
481        void dropOrphanedData (MDB_dbi dbi)
482        {
483            MDB_cursorp cur;
484
485            auto txn = newTransaction ();
486            scope (success) commitTransaction (txn);
487            scope (failure) quitTransaction (txn);
488
489            auto res = txn.mdb_cursor_open (dbi, &cur);
490            scope (exit) cur.mdb_cursor_close ();
491            checkError (res, "mdb_cursor_open (stats)");
492
493            MDB_val ckey;
494            while (cur.mdb_cursor_get (&ckey, null, MDB_NEXT) == 0) {
495                immutable gcid = to!string (fromStringz (cast(char*) ckey.mv_data));
496                if (gcidReferenced (gcid))
497                    continue;
498
499                // if we got here, the component is cruft and can be removed
500                res = cur.mdb_cursor_del (0);
501                checkError (res, "mdb_del");
502                logInfo ("Marked %s as cruft.", gcid);
503            }
504        }
505
506        bool dirEmpty (string dir)
507        {
508            bool empty = true;
509            foreach (ref e; dirEntries (dir, SpanMode.shallow, false)) {
510                empty = false;
511                break;
512            }
513            return empty;
514        }
515
516        void cleanupDirs (string rootPath) {
517            auto pdir = buildNormalizedPath (rootPath, "..");
518            if (!std.file.exists (pdir))
519                return;
520
521            if (dirEmpty (pdir))
522                rmdir (pdir);
523            pdir = buildNormalizedPath (pdir, "..");
524            if (dirEmpty (pdir))
525                rmdir (pdir);
526        }
527
528        // drop orphaned metadata
529        dropOrphanedData (dbDataXml);
530        dropOrphanedData (dbDataYaml);
531
532        // we need the global Config instance here
533        auto conf = Config.get ();
534
535        auto mdirLen = mediaDir.length;
536        foreach (ref path; dirEntries (mediaDir, SpanMode.depth, false)) {
537            if (path.length <= mdirLen)
538                continue;
539            immutable relPath = path[mdirLen+1..$];
540            auto split = array (pathSplitter (relPath));
541            if (split.length != 4)
542                continue;
543            immutable gcid = relPath;
544
545            if (gcidReferenced (gcid))
546                continue;
547
548            // if we are here, the component is removed and we can drop its media
549            if (std.file.exists (path))
550                rmdirRecurse (path);
551
552            // remove possibly empty directories
553            cleanupDirs (path);
554
555            // expire data in suite-specific media directories,
556            // if suite is not marked as immutable
557            if (conf.featureEnabled (GeneratorFeature.IMMUTABLE_SUITES)) {
558                foreach (ref suite; conf.suites) {
559                    if (suite.isImmutable)
560                        continue;
561                    immutable suiteGCIDMediaDir = buildNormalizedPath (mediaDir, "..", suite.name, gcid);
562
563                    if (std.file.exists (suiteGCIDMediaDir))
564                        rmdirRecurse (suiteGCIDMediaDir);
565
566                    // remove possibly empty directories
567                    cleanupDirs (suiteGCIDMediaDir);
568                }
569            }
570
571            logInfo ("Expired media for '%s'", gcid);
572        }
573
574    }
575
576    void removePackagesNotInSet (bool[string] pkgSet)
577    {
578        MDB_cursorp cur;
579
580        auto txn = newTransaction ();
581        scope (success) commitTransaction (txn);
582        scope (failure) quitTransaction (txn);
583
584        auto res = txn.mdb_cursor_open (dbPackages, &cur);
585        scope (exit) cur.mdb_cursor_close ();
586        checkError (res, "mdb_cursor_open (pkgcruft)");
587
588        MDB_val pkey;
589        while (cur.mdb_cursor_get (&pkey, null, MDB_NEXT) == 0) {
590            immutable pkid = to!string (fromStringz (cast(char*) pkey.mv_data));
591            if (pkid in pkgSet)
592                continue;
593
594            // if we got here, the package is not in the set of valid packages,
595            // and we can remove it.
596            res = cur.mdb_cursor_del (0);
597            checkError (res, "mdb_del");
598            logInfo ("Dropped package %s", pkid);
599        }
600    }
601
602    StatisticsEntry[] getStatistics ()
603    {
604        MDB_val dkey, dval;
605        MDB_cursorp cur;
606
607        auto txn = newTransaction (MDB_RDONLY);
608        scope (exit) quitTransaction (txn);
609
610        auto res = txn.mdb_cursor_open (dbStats, &cur);
611        scope (exit) cur.mdb_cursor_close ();
612        checkError (res, "mdb_cursor_open (stats)");
613
614        auto stats = appender!(StatisticsEntry[]);
615        while (cur.mdb_cursor_get (&dkey, &dval, MDB_NEXT) == 0) {
616            auto jsonData = to!string (fromStringz (cast(char*) dval.mv_data));
617            auto timestamp = *(cast(size_t*) dkey.mv_data);
618            auto sentry = StatisticsEntry (timestamp, parseJSON (jsonData));
619            stats ~= sentry;
620        }
621
622        return stats.data;
623    }
624
625    void removeStatistics (size_t time)
626    {
627        MDB_val dbkey;
628
629        dbkey.mv_size = size_t.sizeof;
630        dbkey.mv_data = &time;
631
632        auto txn = newTransaction ();
633        scope (success) commitTransaction (txn);
634        scope (failure) quitTransaction (txn);
635
636        auto res = txn.mdb_del (dbStats, &dbkey, null);
637        if (res != MDB_NOTFOUND)
638            checkError (res, "mdb_del");
639    }
640
641    void addStatistics (JSONValue stats)
642    {
643        import core.stdc.time : time;
644
645        MDB_val dbkey, dbvalue;
646        size_t unixTime = time (null);
647
648        auto statsJsonStr = stats.toString ();
649
650        dbkey.mv_size = size_t.sizeof;
651        dbkey.mv_data = &unixTime;
652        dbvalue = makeDbValue (statsJsonStr);
653
654        auto txn = newTransaction ();
655        scope (success) commitTransaction (txn);
656        scope (failure) quitTransaction (txn);
657
658        auto res = txn.mdb_put (dbStats, &dbkey, &dbvalue, MDB_APPEND);
659        if (res == MDB_KEYEXIST) {
660            // this point in time already exists, so we need to extend it with additional data
661
662            // retrieve the old statistics data
663            auto existingJsonData = getValue (dbStats, dbkey);
664            auto existingJson = parseJSON (existingJsonData);
665
666            // make the new JSON a list of the old and the new data, if it isn't one already
667            JSONValue newJson;
668            if (existingJson.type == JSON_TYPE.ARRAY) {
669                newJson = existingJson;
670                newJson.array ~= stats;
671            } else {
672                newJson = JSONValue ([existingJson, stats]);
673            }
674
675            // build new database value and add it to the db, overriding the old one
676            statsJsonStr = toJSON (&newJson);
677            dbvalue = makeDbValue (statsJsonStr);
678
679            res = txn.mdb_put (dbStats, &dbkey, &dbvalue, 0);
680        }
681        checkError (res, "mdb_put (stats)");
682    }
683
684    JSONValue getRepoInfo (string suite, string section, string arch)
685    {
686        auto repoid = "%s-%s-%s".format (suite, section, arch);
687        auto jsonData = getValue (dbRepoInfo, repoid);
688        if (jsonData is null) {
689            JSONValue[string] dummy;
690            return JSONValue (dummy);
691        }
692
693        return parseJSON (jsonData);
694    }
695
696    void setRepoInfo (string suite, string section, string arch, JSONValue repoInfo)
697    {
698        auto repoid = "%s-%s-%s".format (suite, section, arch);
699        auto jsonData = toJSON (&repoInfo);
700
701        putKeyValue (dbRepoInfo, repoid, jsonData);
702    }
703
704    void removeRepoInfo (string suite, string section, string arch)
705    {
706        auto repoid = "%s-%s-%s".format (suite, section, arch);
707        auto dbkey = makeDbValue (repoid);
708
709        auto txn = newTransaction ();
710        scope (success) commitTransaction (txn);
711        scope (failure) quitTransaction (txn);
712
713        auto res = txn.mdb_del (dbRepoInfo, &dbkey, null);
714        if (res != MDB_NOTFOUND)
715            checkError (res, "mdb_del");
716    }
717
718    /**
719     * Get a list of package-ids which match a prefix.
720     */
721    string[] getPkidsMatching (string prefix)
722    {
723        MDB_val dkey;
724        MDB_cursorp cur;
725        string[long] stats;
726
727        auto txn = newTransaction (MDB_RDONLY);
728        scope (exit) quitTransaction (txn);
729
730        auto res = txn.mdb_cursor_open (dbPackages, &cur);
731        scope (exit) cur.mdb_cursor_close ();
732        checkError (res, "mdb_cursor_open (pkid-match)");
733
734        auto pkids = appender!(string[]);
735        prefix ~= "/";
736        while (cur.mdb_cursor_get (&dkey, null, MDB_NEXT) == 0) {
737            auto pkid = to!string (fromStringz (cast(char*) dkey.mv_data));
738            if (pkid.startsWith (prefix))
739                pkids ~= pkid;
740        }
741
742        return pkids.data;
743    }
744}
Note: See TracBrowser for help on using the repository browser.