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

Last change on this file since 6547 was 6547, checked in by Juanma, 3 years ago

Removed text messages from loadstore class

File size: 13.8 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
15from bs4 import BeautifulSoup
16
17class loadstore:
18        def __init__(self):
19                self.dbg=0
20                self.pluginInfo={'load':'*','load_bundles':'*'}
21                self.store=''
22                self.progress=0
23                self.error=0
24                self.bundleDir="/var/lib/lliurexstore/bundles"
25                self.bundleTypes=['appimg']
26                self.zmdCatalogueDir='/var/lib/lliurexstore/zmds'
27        #def __init__
28
29        def set_debug(self,dbg='1'):
30                self.dbg=int(dbg)
31                self._debug ("Debug enabled")
32        #def set_debug
33
34        def _debug(self,msg=''):
35                if self.dbg==1:
36                        print ('DEBUG Load: '+str(msg))
37        #def _debug
38
39        def register(self):
40                return(self.pluginInfo)
41        #def register
42
43        def execute_action(self,action,store=None,loadBundles=False):
44                self.progress=0
45                if store:
46                        self.store=store
47                else:
48                        self.store=appstream.Store()
49                if action=='load':
50                        self._load_store(self.store)
51                if action=='load_bundles':
52                        self._load_store(self.store,loadBundles=True)
53                self.progress=100
54                return (self.store)
55        #def execute_action
56
57        def get_error(self):
58                return (self.error)
59        #def get_error
60
61        def _load_store(self,store,loadBundles=False):
62                iconPath='/usr/share/icons/hicolor/128x128'
63                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]
64                for flag in flags:
65                        try:
66                                self._debug("Loading "+str(flag))
67                                store.load(flag)
68                        except:
69                                print ("Failed to load"+str(flag))
70                                pass
71                store=self.load_zmds_catalog(store)
72                store=self._sanitize_store(store)
73                if loadBundles:
74                        if self._download_bundles_catalogue():
75                                store=self.load_bundles_catalog(store)
76                self.store=store
77                return(store)
78        #def load_store
79
80        def load_bundles_catalog(self,store):
81                if os.path.exists(self.bundleDir):
82                        for bundleType in self.bundleTypes:
83                                store=self._generic_file_load(self.bundleDir+'/'+bundleType,store)
84                return(store)
85        #def load_bundles_catalog(self)
86
87        def load_zmds_catalog(self,store):
88                if os.path.exists(self.zmdCatalogueDir):
89                        store=self._generic_file_load(self.zmdCatalogueDir,store)
90                return(store)
91        #def load_zmds_catalog(self)
92
93        def _generic_file_load(self,filesDir,store):
94                iconPath='/usr/share/icons/hicolor/128x128'
95                files=os.listdir(filesDir)
96                for appFile in os.listdir(filesDir):
97                        if appFile.endswith('appdata.xml'):
98                                storePath=Gio.File.new_for_path(filesDir+'/'+appFile)
99                                self._debug("Adding file "+filesDir+'/'+appFile)
100                                try:
101                                        store.from_file(storePath,iconPath,None)
102                                except Exception as e:
103                                        self._debug("Couldn't add file "+appFile+" to store")
104                                        self._debug("Reason: "+str(e))
105                return(store)
106       
107        def _parse_desktop(self,store): #DEPRECATED. Loads the apps from the available desktop files
108                desktopDir='/usr/share/applications'
109                applist=[]
110                for desktopFile in os.listdir(desktopDir):
111                        if desktopFile.endswith('desktop'):
112                                a=appstream.App()
113                                try:
114                                        a.parse_file(desktopDir+'/'+desktopFile,16)
115                                        a.set_priority(0)
116                                        for veto in a.get_vetos():
117                                                a.remove_veto(veto)
118                                        store.add_app(a)
119                                        self._debug("Adding app from desktop "+desktopFile)
120                                except:
121                                        pass
122                return(store)
123        #def _parse_desktop
124
125        def _sanitize_store(self,store):
126                applist=store.get_apps()
127                uniqDict={}
128                lliurexApps={}
129                zmdList=[]
130                for app in applist:
131                        #Zomandos get max priority
132                        if app.has_category('Zomando'):
133                                self._debug("Prioritize zmd "+str(app.get_id()))
134                                app.set_priority(400)
135                                lliurexApps.update({app.get_id_filename():app})
136                                id=str(app.get_id_filename()).replace('zero-lliurex-','')
137                                zmdList.append(id)
138                        #Prioritize Lliurex apps
139                        elif app.has_category('Lliurex'):
140                                self._debug("Prioritize app "+str(app.get_id()))
141                                app.set_priority(200)
142                                lliurexApps.update({app.get_id_filename():app})
143                        elif str(app.get_origin()).find('lliurex')>=0:
144                                self._debug("Prioritize app "+app.get_id())
145                                app.set_priority(100)
146                                lliurexApps.update({app.get_id_filename():app})
147                        else:
148                                app.set_priority(0)
149                                if app.get_id_filename() in lliurexApps.keys():
150                                        self._debug("Mergin app "+str(app.get_id())+" as is in Lliurex")
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 and ".desktop" not in fn:
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                                                store.remove_app(uniqDict[pkg]['app'])
173                                                uniqDict.update({pkg:{'fn':app.get_id_no_prefix(),'app':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 resultRe:
221                                                if blApp[0]=='*':
222                                                        blApp='.'+blApp
223                                                blRegEx.append("("+blApp+")")
224                                        else:
225                                                app=store.get_app_by_pkgname(blApp)
226                                                if app:
227                                                        self._debug("Removed "+str(app))
228                                                        store.remove_app(app)
229                                                else:
230                                                        self._debug("App "+blApp+" from blacklist not found in store. Assigned to RE blacklist")
231                                                        blRegEx.append("("+blApp+")")
232                                if blRegEx:
233                                        self._debug("Attempting to remove apps by RE match")
234                                        for app in store.get_apps():
235                                                for blApp in blRegEx:
236                                                        resultRe=re.search(blApp,app.get_id())
237                                                        if resultRe:
238                                                                store.remove_app(app)
239                                                                self._debug("Removed "+str(app.get_id()) +" as matches with "+blApp)
240                        else:
241                                self._debug('No blacklist to check')
242                except Exception as e:
243                        self._debug("Error processing blacklist: "+str(e))
244                finally:
245                        return(store)
246        #def _apply_blacklist
247
248        def _download_bundles_catalogue(self):
249                CURSOR_UP='\033[F'
250                ERASE_LINE='\033[K'
251                content=''
252                applist=[]
253                progressBar="#"
254                repoList={'appimg':['https://dl.bintray.com/probono/AppImages']}
255                #For get the description of an app we must go to a specific url.
256                #$(appname) we'll be replaced with the appname so the url matches the right one.
257                #If other site has other url naming convention it'll be mandatory to define it with the appropiate replacements
258                infoList={'appimg':'https://bintray.com/probono/AppImages/$(appname)'}
259                self.descDict={}
260
261                for repoType,repoTypeList in repoList.items():
262                        infoPage=infoList[repoType]
263                        outdir=self.bundleDir+'/'+repoType+'/'
264                        if self._chk_bundle_dir(outdir):
265                                for repo in repoTypeList:
266                                        self._debug(("Fetching repo %s")%(repo))
267#                                       print (("Fetching %s catalogue: "+progressBar)%repoType,end="\r")
268#                                       progressBar=progressBar+"#"
269#                                       print (("Fetching %s catalogue: "+progressBar)%repoType,end="\r")
270                                        applist=self._generate_applist(self._fetch_repo(repo))
271#                                       progressBar=progressBar+"##"
272#                                       print (("Fetching %s catalogue: "+progressBar)%repoType,end="\r")
273                                        self._debug("Processing info...")
274                                        self._th_generate_xml_catalog(applist,outdir,infoPage,repoType,progressBar)
275                                        self._debug("Fetched repo "+repo)
276#                                       print (("Removing old entries..."))
277                                        self._clean_bundle_catalogue(applist,outdir)
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 _th_generate_xml_catalog(self,applist,outdir,infoPage,repoType,progressBar=''):
307                CURSOR_UP='\033[F'
308                ERASE_LINE='\033[K'
309                oldName=''
310                oldDesc=''
311                maxconnections = 10
312                semaphore = threading.BoundedSemaphore(value=maxconnections)
313                randomList = list(applist)
314                random.shuffle(randomList)
315                lenAppList=len(randomList)
316                inc=30/lenAppList
317#               print (CURSOR_UP)
318                for app in randomList:
319                        th=threading.Thread(target=self._th_write_xml, args = (app,outdir,infoPage,semaphore,inc))
320                        th.start()
321                os.system('setterm -cursor off')
322                while threading.active_count()>2: #Discard both main and own threads
323                        for i in range(len(progressBar),int(self.progress)):
324                                progressBar='#'+progressBar
325#                       print (CURSOR_UP)
326#                       print (("Fetching %s catalogue: "+progressBar)%repoType,end="\r")
327                os.system('setterm -cursor on')
328                print('')
329
330        def _th_write_xml(self,app,outdir,infoPage,semaphore,inc):
331                semaphore.acquire()
332                lock=threading.Lock()
333                nameSplitted=app.split('-')
334                name=nameSplitted[0]
335                version=nameSplitted[1]
336                arch=nameSplitted[2]
337                filename=outdir+app.lower().replace('appimage',"appdata.xml")
338                self._debug("checking if we need to download "+filename)
339                if not os.path.isfile(filename):
340                        self._debug("Generating "+app+" xml")
341                        f=open(filename,'w')
342                        f.write('<?xml version="1.0" encoding="UTF-8"?>'+"\n")
343                        f.write("<components version=\"0.10\">\n")
344                        f.write("<component  type=\"desktop-application\">\n")
345                        f.write("  <id>"+app.lower()+"</id>\n")
346                        f.write("  <pkgname>"+app+"</pkgname>\n")
347                        f.write("  <name>"+name+"</name>\n")
348                        f.write("  <metadata_license>CC0-1.0</metadata_license>\n")
349                        f.write("  <provides><binary>"+app+"</binary></provides>\n")
350                        f.write("  <releases>\n")
351                        f.write("  <release version=\""+version+"\" timestamp=\"1408573857\"></release>\n")
352                        f.write("  </releases>\n")
353                        f.write("  <launchable type=\"desktop-id\">"+name+".desktop</launchable>\n")
354                        with lock:
355                                if name in self.descDict.keys():
356                                        description=self.descDict[name]
357                                else:
358                                        description=self._get_description(name,infoPage)
359                                        self.descDict.update({name:description})
360                        summary=' '.join(list(description.split(' ')[:8]))
361                        description="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."
362                        if not summary:
363                                summary=' '.join(list(description.split(' ')[:8]))
364                        f.write("  <description><p></p><p>"+description+"</p></description>\n")
365                        f.write("  <summary>"+summary+"...</summary>\n")
366                        f.write("  <bundle type=\"appimage\">"+app+"</bundle>\n")
367                        f.write("  <keywords>\n")
368                        f.write("       <keyword>"+name+"</keyword>\n")
369                        f.write("       <keyword>appimage</keyword>\n")
370                        f.write("  </keywords>\n")
371                        f.write("  <categories>\n")
372                        f.write("       <category>AppImage</category>\n")
373                        f.write("       <category>GTK</category>\n")
374                        f.write("  </categories>\n")
375                        f.write("<icon type=\"cached\">"+name+"_"+name+".png</icon>\n")
376                        f.write("</component>\n")
377                        f.write("</components>\n")
378                        f.close()
379                with lock:
380                        self.progress=self.progress+inc
381                semaphore.release()
382       
383        def _get_description(self,appName,infoPage):
384                desc=''
385                if '$(appname)' in infoPage:
386                        infoPage=infoPage.replace('$(appname)',appName)
387                self._debug("Getting description from "+infoPage)
388                try:
389                        with urllib.request.urlopen(infoPage) as f:
390                                content=(f.read().decode('utf-8'))
391                                soup=BeautifulSoup(content,"html.parser")
392                                descDiv=soup.findAll('div', attrs={ "class" : "description-text"})
393                        if len(descDiv)>0:
394                                desc=descDiv[0].text
395                                desc=desc.replace(':','.')
396                                desc=desc.replace('&','&amp;')
397                except Exception as e:
398                        print("Can't get description from "+infoPage)
399                        print(str(e))
400                        pass
401                return(desc)
402
403        def _clean_bundle_catalogue(self,applist,outdir):
404                xmlList=[]
405                applist=[item.lower() for item in applist]
406                for xmlFile in os.listdir(outdir):
407                        if xmlFile.endswith('appdata.xml'):
408                                xmlList.append(xmlFile.lower().replace('appdata.xml','appimage'))
409       
410                if xmlList:
411                        discardList=list(set(xmlList).difference(applist))
412                        for discardFile in discardList:
413                                os.remove(outdir+'/'+discardFile.replace('appimage','appdata.xml'))
414        #def _clean_appImg_repo
415
Note: See TracBrowser for help on using the repository browser.