source: lliurex-store/trunk/fuentes/python3-lliurex-store.install/usr/share/lliurexstore/plugins/loadStore.py @ 5200

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

minor changes

File size: 13.1 KB
Line 
1import os
2import gi
3from gi.repository import Gio
4gi.require_version('AppStreamGlib', '1.0')
5from gi.repository import AppStreamGlib as appstream
6import subprocess
7import json
8import re
9import urllib
10import random
11import threading
12import time
13import datetime
14import gettext
15gettext.textdomain('python3-lliurex-store')
16_=gettext.gettext
17
18class loadstore:
19        def __init__(self):
20                self.dbg=0
21                self.pluginInfo={'load':'*'}
22                self.store=''
23                self.progress=0
24                self.error=0
25                self.bundleDir="/var/lib/lliurexstore/bundles"
26                self.bundleTypes=['appimg']
27                self.zmdCatalogueDir='/var/lib/lliurexstore/zmds'
28        #def __init__
29
30        def set_debug(self,dbg='1'):
31                self.dbg=int(dbg)
32                self._debug ("Debug enabled")
33        #def set_debug
34
35        def _debug(self,msg=''):
36                if self.dbg==1:
37                        print ('DEBUG Load: '+str(msg))
38        #def _debug
39
40        def register(self):
41                return(self.pluginInfo)
42        #def register
43
44        def execute_action(self,action,store=None,loadBundles=False):
45                self.progress=0
46                if store:
47                        self.store=store
48                else:
49                        self.store=appstream.Store()
50                if action=='load':
51                        self._load_store(self.store,loadBundles)
52#               self.progress=100
53                return (self.store)
54        #def execute_action
55
56        def get_error(self):
57                return (self.error)
58        #def get_error
59
60        def _load_store(self,store,loadBundles=False):
61                iconPath='/usr/share/icons/hicolor/128x128'
62                flags=[appstream.StoreLoadFlags.APP_INFO_SYSTEM,appstream.StoreLoadFlags.APP_INSTALL,appstream.StoreLoadFlags.APP_INFO_USER,appstream.StoreLoadFlags.DESKTOP,appstream.StoreLoadFlags.APPDATA,appstream.StoreLoadFlags.ALLOW_VETO]
63                for flag in flags:
64                        try:
65                                self._debug("Loading "+str(flag))
66                                store.load(flag)
67                        except:
68                                print ("Failed to load"+str(flag))
69                                pass
70                store=self.load_zmds_catalog(store)
71                store=self._sanitize_store(store)
72                if loadBundles:
73                        if self._download_bundles_catalogue():
74                                store=self.load_bundles_catalog(store)
75                self.store=store
76                return(store)
77        #def load_store
78
79        def load_bundles_catalog(self,store):
80                if os.path.exists(self.bundleDir):
81                        for bundleType in self.bundleTypes:
82                                store=self._generic_file_load(self.bundleDir+'/'+bundleType,store)
83                return(store)
84        #def load_bundles_catalog(self)
85
86        def load_zmds_catalog(self,store):
87                if os.path.exists(self.zmdCatalogueDir):
88                        store=self._generic_file_load(self.zmdCatalogueDir,store)
89                return(store)
90        #def load_zmds_catalog(self)
91
92        def _generic_file_load(self,filesDir,store):
93                iconPath='/usr/share/icons/hicolor/128x128'
94                for appFile in os.listdir(filesDir):
95                        if appFile.endswith('appdata.xml'):
96                                storePath=Gio.File.new_for_path(filesDir+'/'+appFile)
97                                self._debug("Adding file "+filesDir+'/'+appFile)
98                                try:
99                                        store.from_file(storePath,iconPath,None)
100                                except Exception as e:
101                                        self._debug("Couldn't add file "+appFile+" to store")
102                                        self._debug("Reason: "+str(e))
103                return(store)
104       
105        def _parse_desktop(self,store): #DEPRECATED. Loads the apps from the available desktop files
106                desktopDir='/usr/share/applications'
107                applist=[]
108                for desktopFile in os.listdir(desktopDir):
109                        if desktopFile.endswith('desktop'):
110                                a=appstream.App()
111                                try:
112                                        a.parse_file(desktopDir+'/'+desktopFile,16)
113                                        a.set_priority(0)
114                                        for veto in a.get_vetos():
115                                                a.remove_veto(veto)
116                                        store.add_app(a)
117                                        self._debug("Adding app from desktop "+desktopFile)
118                                except:
119                                        pass
120                return(store)
121        #def _parse_desktop
122
123        def _sanitize_store(self,store):
124                applist=store.get_apps()
125                uniqDict={}
126                lliurexApps={}
127                zmdList=[]
128                for app in applist:
129                        #Zomandos get max priority
130                        if app.has_category('Zomando'):
131                                self._debug("Prioritize zmd "+str(app.get_id()))
132                                app.set_priority(400)
133                                lliurexApps.update({app.get_id_filename():app})
134                                id=str(app.get_id_filename()).replace('zero-lliurex-','')
135                                zmdList.append(id)
136                        #Prioritize Lliurex apps
137                        elif app.has_category('Lliurex'):
138                                self._debug("Prioritize app "+str(app.get_id()))
139                                app.set_priority(200)
140                                lliurexApps.update({app.get_id_filename():app})
141                        elif str(app.get_origin()).find('lliurex')>=0:
142                                self._debug("Prioritize app "+app.get_id())
143                                app.set_priority(100)
144                                lliurexApps.update({app.get_id_filename():app})
145                        else:
146                                app.set_priority(0)
147                                if app.get_id_filename() in lliurexApps.keys():
148#                                       app.set_merge_kind(appstream.AppMergeKind.APPEND)
149                                        self._debug("Mergin app "+str(app.get_id())+" as is in Lliurex")
150#                                       app.set_id(lliurexApps[app.get_id_filename()])
151                                        lliurexApps[app.get_id_filename()].subsume_full(app,appstream.AppSubsumeFlags.BOTH_WAYS)
152                                        store.remove_app(app)
153                        #Remove apps whitout pkgname
154                        if not app.get_pkgnames():
155                                store.remove_app(app)
156                        #Remove add-on apps (as are included in the main packages)
157                        if app.get_kind()==appstream.AppKind.ADDON:
158                                self._debug("Removed addon "+str(app.get_pkgnames()))
159                                store.remove_app(app)
160                        #Remove duplicated apps
161                        #Unlike gnome-store we'll try to compare the info of the package in order of discard only the "part-of" packages
162                        pkg=app.get_pkgname_default()
163                        if pkg in uniqDict.keys():
164                                fn=app.get_id_no_prefix()
165                                self._debug("Comparing "+fn+" with "+uniqDict[pkg]['fn'])
166                                if fn != uniqDict[pkg]['fn']:
167                                        if fn != pkg:
168                                                self._debug("Removed duplicated "+app.get_id())
169                                                store.remove_app(app)
170                                        else:
171                                                self._debug("Removed duplicated "+uniqDict[pkg]['app'].get_id())
172                                                uniqDict.update({pkg:{'fn':app.get_id_no_prefix(),'app':app}})
173                                                store.remove_app(uniqDict[pkg]['app'])
174                        elif pkg:
175#                               self._debug("Adding "+app.get_id_filename()+" to uniq dict")
176                                uniqDict.update({pkg:{'fn':app.get_id_filename(),'app':app}})
177                #Delete zomando-related debs
178                store=self._purge_zomandos(zmdList,store)
179                #Check the blacklist
180                store=self._apply_blacklist(store)
181                return (store)
182        #def _sanitize_store
183
184        def _purge_zomandos(self,zmdList,store):
185                for appId in zmdList:
186                        self._debug("Searching debs related to "+appId)
187                        purgeList=store.get_apps_by_id(appId)
188                        purgeList.extend(store.get_apps_by_id(appId+".desktop"))
189                        for purgeApp in purgeList:
190                                if purgeApp:
191                                        if not purgeApp.has_category('Zomando'):
192                                                self._debug("Removed related zomando app "+str(purgeApp.get_id()))
193                                                store.remove_app(purgeApp)
194                return(store)
195        #def _purge_zomandos
196
197        def _apply_blacklist(self,store):
198                try:
199                        flavour=subprocess.check_output(["lliurex-version","-f"]).rstrip()
200                        flavour=flavour.decode("utf-8")
201                        if flavour=='None':
202                                self._debug("Unknown flavour. Switching to desktop")
203                                flavour='desktop'
204                except (subprocess.CalledProcessError,FileNotFoundError) as e:
205                                self._debug("Running on a non Lliurex host")
206                                flavour='desktop'
207                try:
208                        if os.path.isfile('/usr/share/lliurex-store/files/blacklist.json'):
209                                blFile=open('/usr/share/lliurex-store/files/blacklist.json').read()
210                                blacklist=json.loads(blFile)
211                                blApps=[]
212                                if flavour in blacklist:
213                                        blApps=blacklist[flavour]
214                                if "all" in blacklist:
215                                        blApps.extend(blacklist["all"])
216                                blRegEx=[]
217                                for blApp in blApps:
218                                        self._debug("Blacklisted app: "+blApp)
219                                        resultRe=re.search('([^a-zA-Z0-9_-])',blApp)
220#                                       if blApp[-1]!='*':
221                                        if resultRe:
222                                                if blApp[0]=='*':
223                                                        blApp='.'+blApp
224                                                blRegEx.append("("+blApp+")")
225                                        else:
226                                                app=store.get_app_by_pkgname(blApp)
227                                                if app:
228                                                        self._debug("Removed "+str(app))
229                                                        store.remove_app(app)
230                                                else:
231                                                        self._debug("App "+blApp+" from blacklist not found in store. Assigned to RE blacklist")
232                                                        blRegEx.append("("+blApp+")")
233                                if blRegEx:
234                                        self._debug("Attempting to remove apps by RE match")
235                                        for app in store.get_apps():
236                                                for blApp in blRegEx:
237                                                        resultRe=re.search(blApp,app.get_id())
238                                                        if resultRe:
239#                                                       if blApp.lower() in app.get_id_filename().lower():
240                                                                store.remove_app(app)
241                                                                self._debug("Removed "+str(app.get_id()) +" as matches with "+blApp)
242                        else:
243                                self._debug('No blacklist to check')
244                except Exception as e:
245                        self._debug("Error processing blacklist: "+str(e))
246                finally:
247                        return(store)
248        #def _apply_blacklist
249
250        def _download_bundles_catalogue(self):
251                CURSOR_UP='\033[F'
252                ERASE_LINE='\033[K'
253                content=''
254                applist=[]
255                progressBar="#"
256                repoList={'appimg':['https://dl.bintray.com/probono/AppImages']}
257                #For get the description of an app we must go to a specific url.
258                #$(appname) we'll be replaced with the appname so the url matches the right one.
259                #If other site has other url naming convention it'll be mandatory to define it with the appropiate replacements
260                infoList={'appimg':'https://bintray.com/probono/AppImages/$(appname)'}
261                self.descDict={}
262
263                for repoType,repoTypeList in repoList.items():
264                        infoPage=infoList[repoType]
265                        outdir=self.bundleDir+'/'+repoType+'/'
266                        if self._chk_bundle_dir(outdir):
267                                for repo in repoTypeList:
268                                        self._debug(("Fetching repo %s")%(repo))
269                                        print (_("Fetching %s catalogue: "+progressBar)%repoType,end="\r")
270                                        progressBar=progressBar+"#"
271                                        print (_("Fetching %s catalogue: "+progressBar)%repoType,end="\r")
272                                        applist=self._generate_applist(self._fetch_repo(repo))
273                                        progressBar=progressBar+"##"
274                                        print (_("Fetching %s catalogue: "+progressBar)%repoType,end="\r")
275                                        self._debug("Processing info...")
276                                        self._th_generate_xml_catalog(applist,outdir,infoPage,repoType,progressBar)
277                                        self._debug("Fetched repo "+repo)
278                        else:
279                                print("permission denied")
280                return(True)
281
282        def _chk_bundle_dir(self,outdir):
283                msg_status=True
284                if not os.path.isdir(outdir):
285                        try:
286                                os.makedirs(outdir)
287                        except:
288                                msg_status=False
289                return(os.access(outdir,os.W_OK|os.R_OK|os.X_OK|os.F_OK))
290
291        def _fetch_repo(self,repo):
292                with urllib.request.urlopen(repo) as f:
293                        content=(f.read().decode('utf-8'))
294                return(content)
295
296        def _generate_applist(self,content):
297                garbageList=[]
298                applist=[]
299                garbageList=content.split(' ')
300                for garbageLine in garbageList:
301                        if garbageLine.endswith('AppImage"'):
302                                app=garbageLine.replace('href=":','')
303                                applist.append(app.replace('"',''))
304                return(applist)
305
306        def _get_description(self,appName,infoPage):
307                desc=''
308                if '$(appname)' in infoPage:
309                        infoPage=infoPage.replace('$(appname)',appName)
310                self._debug("Getting description from "+infoPage)
311                try:
312                        with urllib.request.urlopen(infoPage) as f:
313                                content=(f.read().decode('utf-8'))
314                                soup=BeautifulSoup(content,"html.parser")
315                                descDiv=soup.findAll('div', attrs={ "class" : "description-text"})
316                        if len(descDiv)>0:
317                                desc=descDiv[0].text
318                                desc=desc.replace(':','.')
319                                desc=desc.replace('&','&')
320                except:
321                        pass
322                return(desc)
323
324        def _th_generate_xml_catalog(self,applist,outdir,infoPage,repoType,progressBar=''):
325                CURSOR_UP='\033[F'
326                ERASE_LINE='\033[K'
327                oldName=''
328                oldDesc=''
329                maxconnections = 10
330                semaphore = threading.BoundedSemaphore(value=maxconnections)
331                randomList = list(applist)
332                random.shuffle(randomList)
333                lenAppList=len(randomList)
334                inc=30/lenAppList
335                print (CURSOR_UP)
336                for app in randomList:
337                        th=threading.Thread(target=self._th_write_xml, args = (app,outdir,infoPage,semaphore,inc))
338                        th.start()
339                os.system('setterm -cursor off')
340                while threading.active_count()>2: #Discard both main and own threads
341                        for i in range(len(progressBar),int(self.progress)):
342                                progressBar='#'+progressBar
343#                       print (CURSOR_UP)
344                        print (_("Fetching %s catalogue: "+progressBar)%repoType,end="\r")
345                os.system('setterm -cursor on')
346                print('')
347
348        def _th_write_xml(self,app,outdir,infoPage,semaphore,inc):
349                semaphore.acquire()
350                lock=threading.Lock()
351                nameSplitted=app.split('-')
352                name=nameSplitted[0]
353                version=nameSplitted[1]
354                arch=nameSplitted[2]
355                filename=outdir+name+"_"+version+".appdata.xml"
356                self._debug("checking if we need to download "+filename)
357                if not os.path.isfile(filename):
358                        self._debug("Generating "+app+" xml")
359                        f=open(filename,'w')
360                        f.write('<?xml version="1.0" encoding="UTF-8"?>'+"\n")
361                        f.write("<components version=\"0.10\">\n")
362                        f.write("<component  type=\"desktop-application\">\n")
363                        f.write("  <id>"+app.lower()+"</id>\n")
364                        f.write("  <pkgname>"+app+"</pkgname>\n")
365                        f.write("  <name>"+name+"</name>\n")
366                        f.write("  <summary>"+name+" AppImage Bundle</summary>\n")
367                        f.write("  <metadata_license>CC0-1.0</metadata_license>\n")
368                        f.write("  <provides><binary>"+app+"</binary></provides>\n")
369                        f.write("  <releases>\n")
370                        f.write("  <release version=\""+version+"\" timestamp=\"1408573857\"></release>\n")
371                        f.write("  </releases>\n")
372                        f.write("  <launchable type=\"desktop-id\">"+name+".desktop</launchable>\n")
373                        with lock:
374                                if name in self.descDict.keys():
375                                        description=self.descDict[name]
376                                else:
377                                        description=self._get_description(name,infoPage)
378                                        self.descDict.update({name:description})
379                        f.write("  <description><p>This is an AppImage bundle of app "+name+". It hasn't been tested by our developers and comes from a 3rd party dev team. Please use it carefully.</p><p>"+description+"</p></description>\n")
380                        f.write("  <bundle type=\"appimage\">"+app+"</bundle>\n")
381                        f.write("  <keywords>\n")
382                        f.write("       <keyword>"+name+"</keyword>\n")
383                        f.write("       <keyword>appimage</keyword>\n")
384                        f.write("  </keywords>\n")
385                        f.write("  <categories>\n")
386                        f.write("       <category>AppImage</category>\n")
387                        f.write("       <category>GTK</category>\n")
388                        f.write("  </categories>\n")
389                        f.write("<icon type=\"cached\">"+name+"_"+name+".png</icon>\n")
390                        f.write("</component>\n")
391                        f.write("</components>\n")
392                        f.close()
393                with lock:
394                        self.progress=self.progress+inc
395                semaphore.release()
Note: See TracBrowser for help on using the repository browser.