source: appstream-generator/src/asgen/backends/debian/debpkgindex.d @ 4841

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

Initial release

File size: 9.7 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.backends.debian.debpkgindex;
21
22import std.stdio;
23import std.path;
24import std.string;
25import std.algorithm : remove;
26import std.array : appender;
27import std.conv : to;
28static import std.file;
29
30import asgen.logging;
31import asgen.backends.interfaces;
32import asgen.backends.debian.tagfile;
33import asgen.backends.debian.debpkg;
34import asgen.backends.debian.debutils;
35import asgen.config;
36import asgen.utils : escapeXml, getFileContents, isRemote;
37
38
39class DebianPackageIndex : PackageIndex
40{
41
42private:
43    string rootDir;
44    Package[][string] pkgCache;
45    bool[string] indexChanged;
46
47protected:
48    string tmpDir;
49
50public:
51
52    this (string dir)
53    {
54        this.rootDir = dir;
55        if (!dir.isRemote && !std.file.exists (dir))
56            throw new Exception ("Directory '%s' does not exist.".format (dir));
57
58        auto conf = Config.get ();
59        tmpDir = buildPath (conf.getTmpDir, dir.baseName);
60    }
61
62    final void release ()
63    {
64        pkgCache = null;
65        indexChanged = null;
66    }
67
68    private final immutable(string[]) findTranslations (const string suite, const string section)
69    {
70        import std.regex : matchFirst, regex;
71
72        immutable inRelease = buildPath (rootDir, "dists", suite, "InRelease");
73        auto translationregex = r"%s/i18n/Translation-(\w+)$".format (section).regex;
74        bool[string] ret;
75
76        try {
77            synchronized (this) {
78                const inReleaseContents = getFileContents (inRelease);
79
80                foreach (const ref entry; inReleaseContents) {
81                    auto match = entry.matchFirst (translationregex);
82
83                    if (match.empty)
84                        continue;
85
86                    ret[match[1]] = true;
87                }
88            }
89        } catch (Exception ex) {
90            logWarning ("Could not get %s, will assume 'en' is available.", inRelease);
91            return ["en"];
92        }
93
94        return cast(immutable) ret.keys;
95    }
96
97    private final void loadPackageLongDescs (DebPackage[string] pkgs, string suite, string section)
98    {
99        immutable langs = findTranslations (suite, section);
100
101        logDebug ("Found translations for: %s", langs.join(", "));
102
103        foreach (const ref lang; langs) {
104            string fname;
105
106            immutable fullPath = buildPath ("dists",
107                                            suite,
108                                            section,
109                                            "i18n",
110                                            /* here we explicitly substitute a
111                                             * "%s", because
112                                             * downloadIfNecessary will put the
113                                             * file extension there */
114                                            "Translation-%s.%s".format(lang, "%s"));
115
116            try {
117                synchronized (this) {
118                    fname = downloadIfNecessary (rootDir, tmpDir, fullPath);
119                }
120            } catch (Exception ex) {
121                logDebug ("No translations for %s in %s/%s", lang, suite, section);
122                continue;
123            }
124
125            auto tagf = new TagFile ();
126            tagf.open (fname);
127
128            do {
129                auto pkgname = tagf.readField ("Package");
130                auto rawDesc  = tagf.readField ("Description-%s".format (lang));
131                if (!pkgname)
132                    continue;
133                if (!rawDesc)
134                    continue;
135
136                auto pkgP = (pkgname in pkgs);
137                if (pkgP is null)
138                    continue;
139
140                auto split = rawDesc.split ("\n");
141                if (split.length < 2)
142                    continue;
143
144                // NOTE: .remove() removes the element, but does not alter the
145                // length of the array. Bug?  (this is why we slice the array
146                // here)
147                split = split[1..$];
148
149                // TODO: We actually need a Markdown-ish parser here if we want
150                // to support listings in package descriptions properly.
151                auto description = appender!string;
152                description ~= "<p>";
153                bool first = true;
154                foreach (l; split) {
155                    if (l.strip () == ".") {
156                        description ~= "</p>\n<p>";
157                        first = true;
158                        continue;
159                    }
160
161                    if (first)
162                        first = false;
163                    else
164                        description ~= " ";
165
166                    description ~= escapeXml (l);
167                }
168                description ~= "</p>";
169
170                if (lang == "en")
171                    (*pkgP).setDescription (description.data, "C");
172
173                (*pkgP).setDescription (description.data, lang);
174            } while (tagf.nextSection ());
175        }
176    }
177
178    private final string getIndexFile (string suite, string section, string arch)
179    {
180        immutable path = buildPath ("dists", suite, section, "binary-%s".format (arch));
181
182        synchronized (this) {
183            return downloadIfNecessary (rootDir, tmpDir, buildPath (path, "Packages.%s"));
184        }
185    }
186
187    protected DebPackage newPackage (string name, string ver, string arch)
188    {
189        return new DebPackage (name, ver, arch);
190    }
191
192    private final DebPackage[] loadPackages (string suite, string section, string arch)
193    {
194        auto indexFname = getIndexFile (suite, section, arch);
195        if (!std.file.exists (indexFname)) {
196            logWarning ("Archive package index file '%s' does not exist.", indexFname);
197            return [];
198        }
199
200        auto tagf = new TagFile ();
201        tagf.open (indexFname);
202        logDebug ("Opened: %s", indexFname);
203
204        DebPackage[string] pkgs;
205        do {
206            auto name  = tagf.readField ("Package");
207            auto ver   = tagf.readField ("Version");
208            auto fname = tagf.readField ("Filename");
209            if (!name)
210                continue;
211
212            auto pkg = newPackage (name, ver, arch);
213            pkg.filename = buildPath (rootDir, fname);
214            pkg.maintainer = tagf.readField ("Maintainer");
215
216            if (!pkg.isValid ()) {
217                logWarning ("Found invalid package (%s)! Skipping it.", pkg.toString ());
218                continue;
219            }
220
221            // filter out the most recent package version in the packages list
222            auto epkgP = name in pkgs;
223            if (epkgP !is null) {
224                auto epkg = *epkgP;
225                if (compareVersions (epkg.ver, pkg.ver) > 0)
226                    continue;
227            }
228
229            pkgs[name] = pkg;
230        } while (tagf.nextSection ());
231
232        // load long descriptions
233        loadPackageLongDescs (pkgs, suite, section);
234
235        return pkgs.values;
236    }
237
238    Package[] packagesFor (string suite, string section, string arch)
239    {
240        immutable id = "%s/%s/%s".format (suite, section, arch);
241        if (id !in pkgCache) {
242            auto pkgs = loadPackages (suite, section, arch);
243            synchronized (this) pkgCache[id] = to!(Package[]) (pkgs);
244        }
245
246        return pkgCache[id];
247    }
248
249    final bool hasChanges (DataStore dstore, string suite, string section, string arch)
250    {
251        import std.json;
252
253        auto indexFname = getIndexFile (suite, section, arch);
254        // if the file doesn't exit, we will emit a warning later anyway, so we just ignore this here
255        if (!std.file.exists (indexFname))
256            return true;
257
258        // check our cache on whether the index had changed
259        if (indexFname in indexChanged)
260            return indexChanged[indexFname];
261
262        std.datetime.SysTime mtime;
263        std.datetime.SysTime atime;
264        std.file.getTimes (indexFname, atime, mtime);
265        auto currentTime = mtime.toUnixTime ();
266
267        auto repoInfo = dstore.getRepoInfo (suite, section, arch);
268        scope (exit) {
269            repoInfo.object["mtime"] = JSONValue (currentTime);
270            dstore.setRepoInfo (suite, section, arch, repoInfo);
271        }
272
273        if ("mtime" !in repoInfo.object) {
274            indexChanged[indexFname] = true;
275            return true;
276        }
277
278        auto pastTime = repoInfo["mtime"].integer;
279        if (pastTime != currentTime) {
280            indexChanged[indexFname] = true;
281            return true;
282        }
283
284        indexChanged[indexFname] = false;
285        return false;
286    }
287}
288
289unittest {
290    import std.algorithm.sorting : sort;
291    import asgen.utils : getTestSamplesDir;
292
293    writeln ("TEST: ", "DebianPackageIndex");
294
295    auto pi = new DebianPackageIndex (buildPath (getTestSamplesDir (), "debian"));
296    assert (sort(pi.findTranslations ("sid", "main").dup) ==
297            sort(["en", "ca", "cs", "da", "de", "de_DE", "el", "eo", "es",
298                   "eu", "fi", "fr", "hr", "hu", "id", "it", "ja", "km", "ko",
299                   "ml", "nb", "nl", "pl", "pt", "pt_BR", "ro", "ru", "sk",
300                   "sr", "sv", "tr", "uk", "vi", "zh", "zh_CN", "zh_TW"]));
301}
Note: See TracBrowser for help on using the repository browser.